diff --git a/packages/icons/build-icons.ts b/packages/icons/build-icons.ts index 20163c8..eb42b7e 100644 --- a/packages/icons/build-icons.ts +++ b/packages/icons/build-icons.ts @@ -5,7 +5,13 @@ import { join } from 'node:path' const root = fileURLToPath(new URL('.', import.meta.url)) const viewBoxes: Record = {} const entries = readdirSync(join(root, 'svg')).filter((name) => name.endsWith('.svg')).sort().map((file) => { - const source = readFileSync(join(root, 'svg', file), 'utf8').replace(//g, '').trim() + let source = readFileSync(join(root, 'svg', file), 'utf8') + let previous: string + do { + previous = source + source = source.replace(//g, '') + } while (source !== previous) + source = source.trim() const match = /^]*)>([\s\S]*)<\/svg>$/.exec(source) const viewBox = match && /viewBox=["']([^"']+)["']/.exec(match[1]!)?.[1] if (!match || !viewBox) throw new Error(`${file}: missing SVG viewBox`) diff --git a/server/analyze.ts b/server/analyze.ts index 513c2e1..1649181 100644 --- a/server/analyze.ts +++ b/server/analyze.ts @@ -1000,7 +1000,7 @@ export function renameSuggestion(suggestion: T, name: string): if (mapping.placement === 'module') { mapping.arrayName = name } else { - const pattern = new RegExp(`\\b${mapping.itemName.replace(/\$/g, '\\$')}\\.`, 'g') + const pattern = new RegExp(`\\b${mapping.itemName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.`, 'g') body = body.replace(pattern, `${name}.`) mapping.key = mapping.key.replace(pattern, `${name}.`) mapping.itemName = name diff --git a/server/devtools.test.ts b/server/devtools.test.ts index fd65bae..41cf86b 100644 --- a/server/devtools.test.ts +++ b/server/devtools.test.ts @@ -11,11 +11,11 @@ import { createDevtoolsServer } from './devtools.ts' const cleanups: (() => void)[] = [] afterEach(() => { for (const cleanup of cleanups.splice(0).reverse()) cleanup() }) -async function fixture() { +async function fixture(editorUrl = (file: string) => `/editor?file=${encodeURIComponent(file)}`) { const running = createApp() const other = createApp() cleanups.push(running.cleanup, other.cleanup) - const devtools = createDevtoolsServer({ root: running.root, editorUrl: (file) => `/editor?file=${encodeURIComponent(file)}` }) + const devtools = createDevtoolsServer({ root: running.root, editorUrl }) const server = createServer((req, res) => devtools.middleware(req, res, () => { res.statusCode = 404; res.end() })) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) cleanups.push(() => { devtools.close(); server.closeAllConnections(); server.close() }) @@ -27,6 +27,20 @@ async function fixture() { return { running, other, devtools, get, post } } +test('unexpected exceptions are logged locally without exposing details over HTTP', async () => { + const error = new Error('Secret project path and stack details') + const logged = spyOn(console, 'error').mockImplementation(() => {}) + try { + const { get } = await fixture(() => { throw error }) + const response = await get('/open-in-editor?file=src/App.btsx') + expect(response.status).toBe(500) + expect(await response.json()).toEqual({ error: 'The devtools request failed. Check the server console for details.' }) + expect(logged).toHaveBeenCalledWith('[beast-devtools] Request failed:', error) + } finally { + logged.mockRestore() + } +}) + test.each(['running', 'opened'])('%s project watcher ignores missing filenames and still emits source changes', async (project) => { const watched = spyOn(fs, 'watch') try { diff --git a/server/devtools.ts b/server/devtools.ts index 8b59087..dd4522e 100644 --- a/server/devtools.ts +++ b/server/devtools.ts @@ -207,7 +207,8 @@ export function createDevtoolsServer(options: DevtoolsServerOptions): DevtoolsSe return send(404, { error: `Unknown endpoint ${url.pathname}` }) } catch (error) { if (error instanceof RefactorError) return send(error.status, { error: error.message }) - return send(500, { error: error instanceof Error ? error.message : String(error) }) + console.error('[beast-devtools] Request failed:', error) + return send(500, { error: 'The devtools request failed. Check the server console for details.' }) } } diff --git a/server/project.ts b/server/project.ts index c7f0cd1..7118487 100644 --- a/server/project.ts +++ b/server/project.ts @@ -413,7 +413,7 @@ function validateName(name: string, suggestion: RefactorSuggestion, source: stri 422, ) } - const used = (word: string) => new RegExp(`(^|[^\\w$])${word.replace(/\$/g, '\\$')}($|[^\\w$])`).test(source) + const used = (word: string) => new RegExp(`(^|[^\\w$])${word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^\\w$])`).test(source) if (used(name)) throw new RefactorError(`${name} is already used in this file. Choose another name.`, 422) if (component && suggestion.propsType !== null && used(`${name}Props`)) { throw new RefactorError(`${name}Props is already used in this file. Choose another name.`, 422) diff --git a/server/style-edits.test.ts b/server/style-edits.test.ts new file mode 100644 index 0000000..56ad32f --- /dev/null +++ b/server/style-edits.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'bun:test' +import { replaceTailwindClasses } from './style-edits.ts' + +test('arbitrary Tailwind values preserve literal backslashes and underscores', () => { + expect(replaceTailwindClasses([], { 'background-image': String.raw`url(foo\bar_baz.png)` })) + .toEqual([String.raw`[background-image:url(foo\\bar\_baz.png)]`]) +}) diff --git a/server/style-edits.ts b/server/style-edits.ts index 1739018..0196eb8 100644 --- a/server/style-edits.ts +++ b/server/style-edits.ts @@ -137,7 +137,7 @@ export function replaceTailwindClasses(classes: string[], declarations: Record