From 10982024d76841578354e239ef40103ec6614a38 Mon Sep 17 00:00:00 2001 From: KazenDev Date: Tue, 15 Sep 2026 15:28:31 -0500 Subject: [PATCH] cli: restore the .scm preload so `cd cli && bun test` starts cli/bunfig.toml lists ../test/setup-scm-loader.ts as a preload and the repo does not contain it, so bun aborts on the first unresolvable preload and no test in the package runs. Add the loader both that config and docs/testing.md already reference, so a .scm query arrives as its text instead of a path. --- cli/src/__tests__/scm-loader.test.ts | 64 ++++++++++++++++++++++++++++ test/setup-scm-loader.ts | 37 ++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 cli/src/__tests__/scm-loader.test.ts create mode 100644 test/setup-scm-loader.ts diff --git a/cli/src/__tests__/scm-loader.test.ts b/cli/src/__tests__/scm-loader.test.ts new file mode 100644 index 0000000000..59f4fee8ed --- /dev/null +++ b/cli/src/__tests__/scm-loader.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from 'bun:test' +import { mkdtemp, rm } from 'fs/promises' +import * as os from 'os' +import * as path from 'path' + +import { scmTextModule } from '../../../test/setup-scm-loader' + +/** + * Import what the loader would emit. The loader's whole job is to hand the + * importer a module instead of a path, so a round trip through a real import is + * the only assertion that covers both the escaping and the default export. + */ +async function importEmitted(text: string): Promise { + const dir = await mkdtemp(path.join(os.tmpdir(), 'scm-loader-')) + const file = path.join(dir, 'query.mjs') + try { + await Bun.write(file, scmTextModule(text)) + const module = (await import(file)) as { default: string } + return module.default + } finally { + await rm(dir, { recursive: true, force: true }) + } +} + +describe('scm-text-loader', () => { + // The loader is a preload for `cd cli && bun test` (cli/bunfig.toml), so its + // test lives with the package that preloads it. + test('hands the importer the file text, not its path', async () => { + const query = + '(method_declaration name: (identifier) @name) @definition.method' + + expect(await importEmitted(query)).toBe(query) + }) + + test('escapes text that would otherwise end the string literal', async () => { + // Real queries carry regexes with backslashes and quoted literals. The + // unescaped form is a syntax error, not a wrong query, so this asserts the + // module still evaluates and still carries every character. + const query = '(string_literal) @literal (#match? @literal "\\\\n")' + + const emitted = scmTextModule(query) + + expect(emitted).not.toContain('\n') + expect(await importEmitted(query)).toBe(query) + }) + + test('round-trips newlines and quotes', async () => { + const query = '(comment) @c\n// "quoted" \\ backslash\n(program) @p' + + expect(await importEmitted(query)).toBe(query) + }) + + test('the queries it loads are real, non-empty text', async () => { + const file = path.join( + __dirname, + '../../../packages/code-map/src/tree-sitter-queries/tree-sitter-c_sharp-tags.scm', + ) + + const text = await Bun.file(file).text() + + expect(text.length).toBeGreaterThan(0) + expect(await importEmitted(text)).toBe(text) + }) +}) diff --git a/test/setup-scm-loader.ts b/test/setup-scm-loader.ts new file mode 100644 index 0000000000..05722fafb5 --- /dev/null +++ b/test/setup-scm-loader.ts @@ -0,0 +1,37 @@ +/** + * Loads `*.scm` tree-sitter query files as text. + * + * `cli/bunfig.toml` lists this file as a preload and `docs/testing.md` passes it + * to `bun test --preload`. Bun's default for an extension it does not know is to + * resolve the import to the file's *path*, so without this the query text + * `@codebuff/code-map` imports through the `@codebuff/sdk` barrel is a path. + * `createLanguageConfig` reads an absolute path back off disk as a fallback + * (`packages/code-map/src/languages.ts`), which is why the CLI still works when + * this loader is missing — but only because the path it gets happens to be + * absolute, and any bundler that inlines the import has no file to read. + * + * bun 1.3.14 has no `loader: 'text'`, so the contents are emitted as a JS module + * whose default export is the JSON-escaped text. + */ +import { plugin } from 'bun' + +/** + * The module bun should hand an importer in place of the `.scm` file. + * + * `JSON.stringify` is both the escaper and the quoting: it produces a literal + * JavaScript can evaluate back to the exact text, so a query containing + * backslashes, quotes or newlines survives the round trip. + */ +export function scmTextModule(text: string): string { + return `export default ${JSON.stringify(text)}` +} + +plugin({ + name: 'scm-text-loader', + setup(build) { + build.onLoad({ filter: /\.scm$/ }, async (args) => ({ + contents: scmTextModule(await Bun.file(args.path).text()), + loader: 'js', + })) + }, +})