Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions cli/src/__tests__/scm-loader.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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)
})
})
37 changes: 37 additions & 0 deletions test/setup-scm-loader.ts
Original file line number Diff line number Diff line change
@@ -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',
}))
},
})
Loading