@mnemonik/shared 7.142.0 → 7.143.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,8 @@
17
17
  * nothing at runtime - which is the whole reason this plan chose WebAssembly
18
18
  * over the native bindings.
19
19
  *
20
- * Run: npx tsx packages/shared/scripts/vendor-grammars.ts [--check]
20
+ * Run: npx tsx packages/shared/scripts/vendor-grammars.ts [--check | --only=<id>]
21
+ * JSX repair: run --only=javascript and --only=tsx; other assets stay untouched.
21
22
  * --check verify every manifest entry is present and non-empty; write nothing.
22
23
  * This is what CI should run to catch a grammar that silently stopped
23
24
  * shipping an artifact.
@@ -25,6 +26,7 @@
25
26
 
26
27
  import { execFileSync } from 'node:child_process';
27
28
  import {
29
+ chmodSync,
28
30
  copyFileSync,
29
31
  existsSync,
30
32
  mkdirSync,
@@ -42,13 +44,13 @@ import { Language, Parser, Query } from 'web-tree-sitter';
42
44
  import { AST_LANGUAGE_IDS, QUERY_CHAIN, grammarArtifactBasename } from '../src/ast/grammars.js';
43
45
 
44
46
  const WASM_DIR = fileURLToPath(new URL('../wasm/', import.meta.url));
45
- /** Queries we author ourselves, for grammars whose upstream ships none. */
47
+ /** Local queries for missing or unsuitable upstream declaration captures. */
46
48
  const OURS_DIR = fileURLToPath(new URL('../queries/', import.meta.url));
47
49
 
48
50
  interface GrammarSpec {
49
51
  /** Vendored artifact basename, and the id the registry keys on. */
50
52
  id: string;
51
- /** npm package to fetch. */
53
+ /** npm package or pinned source archive to fetch with npm pack. */
52
54
  pkg: string;
53
55
  /**
54
56
  * `.wasm` basename inside the tarball when it differs from the package's own
@@ -59,7 +61,7 @@ interface GrammarSpec {
59
61
  /**
60
62
  * Where the tag query comes from. 'upstream' copies queries/tags.scm out of
61
63
  * the tarball; 'ours' copies packages/shared/queries/<id>.tags.scm, which we
62
- * maintain because the grammar ships no tags.scm at all.
64
+ * maintain when the upstream query is absent or captures unsuitable ranges.
63
65
  */
64
66
  tags: 'upstream' | 'ours' | 'inherited';
65
67
  }
@@ -80,7 +82,7 @@ interface GrammarSpec {
80
82
  * predates the current wasm ABI. Presence is not loadability, which is why
81
83
  * --check verifies both. Flutter matters, so this is worth revisiting by
82
84
  * compiling from source with tree-sitter-cli.
83
- * kotlin, swift, lua, perl, zig - bundle no .wasm, so they need a build step
85
+ * kotlin, perl, zig - bundle no .wasm, so they need a build step
84
86
  * with tree-sitter-cli. Deferred until the grammars above are proven; they
85
87
  * keep the heuristic chunker meanwhile.
86
88
  * markdown - keeps chunkMarkdown. Headers already are a document's semantic
@@ -99,7 +101,7 @@ const GRAMMARS: GrammarSpec[] = [
99
101
  wasmFile: 'tree-sitter-tsx.wasm',
100
102
  tags: 'inherited',
101
103
  },
102
- { id: 'javascript', pkg: 'tree-sitter-javascript', tags: 'upstream' },
104
+ { id: 'javascript', pkg: 'tree-sitter-javascript@0.25.0', tags: 'upstream' },
103
105
  { id: 'python', pkg: 'tree-sitter-python', tags: 'upstream' },
104
106
  { id: 'go', pkg: 'tree-sitter-go', tags: 'upstream' },
105
107
  { id: 'java', pkg: 'tree-sitter-java', tags: 'upstream' },
@@ -118,8 +120,29 @@ const GRAMMARS: GrammarSpec[] = [
118
120
  { id: 'elixir', pkg: 'tree-sitter-elixir', tags: 'upstream' },
119
121
  { id: 'solidity', pkg: 'tree-sitter-solidity', tags: 'upstream' },
120
122
  { id: 'bash', pkg: 'tree-sitter-bash', tags: 'ours' },
123
+ {
124
+ // Upstream MIT 0.4.1, gitHead 816840c592ab973500ae9750763c707b447e7fef; shipped ABI-15 WASM.
125
+ id: 'lua',
126
+ pkg: '@tree-sitter-grammars/tree-sitter-lua@0.4.1',
127
+ wasmFile: 'tree-sitter-lua.wasm',
128
+ tags: 'upstream',
129
+ },
121
130
  { id: 'groovy', pkg: 'tree-sitter-groovy', tags: 'ours' },
122
131
  { id: 'powershell', pkg: 'tree-sitter-powershell', tags: 'ours' },
132
+ {
133
+ id: 'swift',
134
+ // Upstream 0.7.3, MIT. Pin source, not an opaque workflow WASM.
135
+ pkg: 'https://codeload.github.com/alex-pinkus/tree-sitter-swift/tar.gz/b8b22bffbb3441780e6471665bacfb263741c86a',
136
+ wasmFile: 'tree-sitter-swift.wasm',
137
+ tags: 'ours',
138
+ },
139
+ {
140
+ id: 'sql',
141
+ // @derekstride/tree-sitter-sql 0.3.11 gitHead, MIT; source includes the external scanner.
142
+ pkg: 'https://codeload.github.com/derekstride/tree-sitter-sql/tar.gz/7b51ecda191d36b92f5a90a8d1bc3faef1c7b8b8',
143
+ wasmFile: 'tree-sitter-sql.wasm',
144
+ tags: 'ours',
145
+ },
123
146
  ];
124
147
 
125
148
  /**
@@ -229,7 +252,7 @@ async function checkOnly(): Promise<Problem[]> {
229
252
  * Source: https://github.com/tree-sitter/tree-sitter-typescript/tree/v0.23.2
230
253
  * MIT; CLI 0.24.4 uses emscripten/emsdk:3.1.64 for the WASM build.
231
254
  */
232
- function rebuildTypescript(dir: string, work: string): void {
255
+ function rebuildTypescript(dir: string, work: string, only?: string): void {
233
256
  const grammarPath = join(dir, 'common', 'define-grammar.js');
234
257
  let grammar = readFileSync(grammarPath, 'utf8');
235
258
  for (const [before, after] of [
@@ -260,6 +283,12 @@ function rebuildTypescript(dir: string, work: string): void {
260
283
  ]);
261
284
  const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
262
285
  for (const dialect of ['typescript', 'tsx']) {
286
+ if (only && only !== dialect) continue;
287
+ // TypeScript has no JSX; keep its existing WASM bytes unchanged.
288
+ if (dialect === 'tsx') {
289
+ patchJsxScanner(join(dir, 'common', 'scanner.h'));
290
+ patchJsxAttributes(join(tools, 'node_modules', 'tree-sitter-javascript', 'grammar.js'));
291
+ }
263
292
  const options = {
264
293
  cwd: join(dir, dialect),
265
294
  env: { ...process.env, NODE_PATH: join(tools, 'node_modules') },
@@ -273,7 +302,235 @@ function rebuildTypescript(dir: string, work: string): void {
273
302
  }
274
303
  }
275
304
 
305
+ /** Upstream f8637eb: preserve entity boundaries while accepting bare JSX ampersands. */
306
+ function patchJsxScanner(scannerPath: string): void {
307
+ execFileSync('patch', ['--batch', scannerPath], {
308
+ input: readFileSync(new URL('./jsx-bare-ampersand.patch', import.meta.url)),
309
+ });
310
+ }
311
+
312
+ /** Bare attribute ampersands are fragments; complete entity tokens retain precedence. */
313
+ function patchJsxAttributes(grammarPath: string): void {
314
+ let grammar = readFileSync(grammarPath, 'utf8');
315
+ for (const [before, after] of [
316
+ [
317
+ 'alias($.unescaped_double_jsx_string_fragment, $.string_fragment),',
318
+ "alias($.unescaped_double_jsx_string_fragment, $.string_fragment),\n alias(token.immediate(prec(-1, '&')), $.string_fragment),",
319
+ ],
320
+ [
321
+ 'alias($.unescaped_single_jsx_string_fragment, $.string_fragment),',
322
+ "alias($.unescaped_single_jsx_string_fragment, $.string_fragment),\n alias(token.immediate(prec(-1, '&')), $.string_fragment),",
323
+ ],
324
+ ['/([^"&]|&[^#A-Za-z])+/', '/[^"&]+/'],
325
+ ["/([^'&]|&[^#A-Za-z])+/", "/[^'&]+/"],
326
+ ] as const) {
327
+ if (!grammar.includes(before)) throw new Error('JSX attribute patch no longer applies');
328
+ grammar = grammar.replace(before, after);
329
+ }
330
+ writeFileSync(grammarPath, grammar);
331
+ }
332
+
333
+ /** 0.25.0's reserved-word DSL needs generator 0.25.8; keep the existing WASM build tool. */
334
+ function rebuildJavascript(dir: string, work: string): void {
335
+ patchJsxScanner(join(dir, 'src', 'scanner.c'));
336
+ patchJsxAttributes(join(dir, 'grammar.js'));
337
+ const generator = join(work, 'javascript-generator');
338
+ execFileSync('npm', [
339
+ 'install',
340
+ '--prefix',
341
+ generator,
342
+ '--no-audit',
343
+ '--no-fund',
344
+ '--no-package-lock',
345
+ 'tree-sitter-cli@0.25.8',
346
+ ]);
347
+ execFileSync(join(generator, 'node_modules', '.bin', 'tree-sitter'), ['generate'], { cwd: dir });
348
+ const tools = join(work, 'javascript-build');
349
+ execFileSync('npm', [
350
+ 'install',
351
+ '--prefix',
352
+ tools,
353
+ '--no-audit',
354
+ '--no-fund',
355
+ '--no-package-lock',
356
+ 'tree-sitter-cli@0.24.4',
357
+ ]);
358
+ // CLI 0.24.4 pins emscripten/emsdk:3.1.64 for --docker.
359
+ execFileSync(
360
+ join(tools, 'node_modules', '.bin', 'tree-sitter'),
361
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-javascript.wasm')],
362
+ { cwd: dir }
363
+ );
364
+ }
365
+
366
+ /** Same CLI/emsdk as the repaired TS grammars; no native binding or grammar dependency. */
367
+ function rebuildSwift(dir: string, work: string): void {
368
+ // A double optional marker must be adjacent to its type. The external ?? token
369
+ // skips whitespace, swallowing `as? Int ?? 0` into optional_type and rejecting 0.
370
+ const grammarPath = join(dir, 'grammar.js');
371
+ const grammar = readFileSync(grammarPath, 'utf8');
372
+ const before = ' alias($._nil_coalescing_operator, "??")';
373
+ if (!grammar.includes(before)) throw new Error('Swift optional-marker patch no longer applies');
374
+ writeFileSync(grammarPath, grammar.replace(before, ' token.immediate("??")'));
375
+ const scannerPath = join(dir, 'src', 'scanner.c');
376
+ const scanner = readFileSync(scannerPath, 'utf8');
377
+ const continuation =
378
+ ' // Let\'s consume operators that can live after a "semicolon" style newline.';
379
+ if (!scanner.includes(continuation))
380
+ throw new Error('Swift multiplication patch no longer applies');
381
+ writeFileSync(
382
+ scannerPath,
383
+ scanner.replace(
384
+ continuation,
385
+ ` // A whitespace-delimited multiplication operator continues the preceding expression.
386
+ // Keep mark_end before '*' so a non-binary spelling can still emit the newline.
387
+ if (ws_directive == CONTINUE_PARSING_TOKEN_FOUND && lookahead == '*') {
388
+ lexer->advance(lexer, false);
389
+ if (iswspace(lexer->lookahead)) return STOP_PARSING_NOTHING_FOUND;
390
+ }
391
+
392
+ ${continuation}`
393
+ )
394
+ );
395
+ const tools = join(work, 'swift-build');
396
+ execFileSync('npm', [
397
+ 'install',
398
+ '--prefix',
399
+ tools,
400
+ '--no-audit',
401
+ '--no-fund',
402
+ '--no-package-lock',
403
+ 'tree-sitter-cli@0.24.4',
404
+ ]);
405
+ const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
406
+ execFileSync(cli, ['generate'], { cwd: dir });
407
+ // CLI 0.24.4 pins emscripten/emsdk:3.1.64 for --docker.
408
+ execFileSync(
409
+ cli,
410
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-swift.wasm')],
411
+ { cwd: dir }
412
+ );
413
+ copyFileSync(join(dir, 'LICENSE'), join(WASM_DIR, 'swift.LICENSE'));
414
+ }
415
+
416
+ /** Upstream package.json pins CLI 0.24.7. SQL function bodies only, not general PL/pgSQL. */
417
+ function rebuildSql(dir: string, work: string): void {
418
+ const grammarPath = join(dir, 'grammar.js');
419
+ let grammar = readFileSync(grammarPath, 'utf8');
420
+ const patch = (before: string, after: string): void => {
421
+ if (!grammar.includes(before)) throw new Error(`SQL patch no longer applies: ${before}`);
422
+ grammar = grammar.replace(before, after);
423
+ };
424
+ // Bounded repairs demonstrated by migrations 289/279/283 and the escaped-name regression.
425
+ const constraint = ` _constraint_literal: $ => seq(
426
+ $.keyword_constraint,
427
+ field('name', $.identifier),
428
+ choice(`;
429
+ patch(constraint, constraint + '\n $._key_constraint,');
430
+ patch('_double_quote_string: _ => /"[^"]*"/', '_double_quote_string: _ => /"([^"]|"")*"/');
431
+ patch(
432
+ 'wrapped_in_parenthesis($.binary_expression)\n ),\n\n _default_expression',
433
+ 'wrapped_in_parenthesis($._expression)\n ),\n\n _default_expression'
434
+ );
435
+ patch(
436
+ 'field("custom_type", $.object_reference)',
437
+ 'seq(field("custom_type", $.object_reference), optional(wrapped_in_parenthesis($._natural_number)))'
438
+ );
439
+ patch(
440
+ ` $.keyword_conflict,
441
+ seq(`,
442
+ ` $.keyword_conflict,
443
+ optional($._column_list),
444
+ seq(`
445
+ );
446
+ patch(
447
+ ` $.keyword_brin
448
+ ),`,
449
+ ` $.keyword_brin,
450
+ $.identifier
451
+ ),`
452
+ );
453
+ patch(
454
+ ` $.index_fields
455
+ ),`,
456
+ ` $.index_fields
457
+ ),
458
+ optional($.storage_parameters),`
459
+ );
460
+ patch(
461
+ ` $.drop_function,
462
+ ),`,
463
+ ` $.drop_function,
464
+ $.drop_trigger,
465
+ ),`
466
+ );
467
+ patch(
468
+ ' drop_table: $ => seq(',
469
+ ` drop_trigger: $ => seq(
470
+ $.keyword_drop, $.keyword_trigger, optional($._if_exists),
471
+ $.identifier, $.keyword_on, $.object_reference, optional($._drop_behavior),
472
+ ),
473
+
474
+ drop_table: $ => seq(`
475
+ );
476
+ // Dollar-quoted procedural code is one literal, never a parsed SQL statement list.
477
+ patch(' $.reset_statement,', ' $.reset_statement,\n $.do_statement,');
478
+ patch(
479
+ ' _cte: $ => seq(',
480
+ ` do_statement: $ => seq($.keyword_do, alias($._dollar_quoted_string, $.literal)),
481
+
482
+ _cte: $ => seq(`
483
+ );
484
+ patch(' conflicts: $ => [', ' conflicts: $ => [\n [$.create_function],');
485
+ const functionStart = grammar.indexOf(' create_function: $ => seq(');
486
+ const functionEnd = grammar.indexOf(' _function_return:', functionStart);
487
+ if (functionStart < 0 || functionEnd < 0) throw new Error('SQL function patch no longer applies');
488
+ const original = grammar.slice(functionStart, functionEnd).trimEnd();
489
+ const languageOption = ' $.function_language,\n';
490
+ const optionsStart = original.indexOf(' repeat(');
491
+ const optionsEnd = original.indexOf(' // ensure');
492
+ const options = original.slice(optionsStart, optionsEnd).replace(languageOption, '');
493
+ // A dedicated keyword selects the opaque branch; ordinary LANGUAGE SQL keeps its AST.
494
+ const procedural =
495
+ original.slice(0, optionsStart) +
496
+ options +
497
+ ` alias(seq($.keyword_language, alias(make_keyword("plpgsql"), $.identifier)), $.function_language),\n` +
498
+ options +
499
+ ` alias(seq($.keyword_as, alias(choice($._dollar_quoted_string, $._single_quote_string, $._double_quote_string), $.literal)), $.function_body),\n` +
500
+ options +
501
+ ' )';
502
+ patch(
503
+ original,
504
+ procedural.replace('create_function: $ => seq(', 'create_function: $ => choice(prec(1, seq(') +
505
+ '),\n' +
506
+ original.slice(original.indexOf('seq('), -1) +
507
+ '),'
508
+ );
509
+ writeFileSync(grammarPath, grammar);
510
+ const tools = join(work, 'sql-build');
511
+ execFileSync('npm', [
512
+ 'install',
513
+ '--prefix',
514
+ tools,
515
+ '--no-audit',
516
+ '--no-fund',
517
+ '--no-package-lock',
518
+ 'tree-sitter-cli@0.24.7',
519
+ ]);
520
+ const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
521
+ execFileSync(cli, ['generate'], { cwd: dir });
522
+ execFileSync(
523
+ cli,
524
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-sql.wasm')],
525
+ { cwd: dir }
526
+ );
527
+ copyFileSync(join(dir, 'LICENSE'), join(WASM_DIR, 'sql.LICENSE'));
528
+ }
529
+
276
530
  function vendor(): Problem[] {
531
+ const only = process.argv.find((arg) => arg.startsWith('--only='))?.slice(7);
532
+ if (only && !GRAMMARS.some((spec) => spec.id === only))
533
+ throw new Error(`Unknown grammar: ${only}`);
277
534
  mkdirSync(WASM_DIR, { recursive: true });
278
535
  const work = mkdtempSync(join(tmpdir(), 'ts-grammars-'));
279
536
  const problems: Problem[] = [];
@@ -283,6 +540,7 @@ function vendor(): Problem[] {
283
540
 
284
541
  try {
285
542
  for (const spec of GRAMMARS) {
543
+ if (only && spec.id !== only) continue;
286
544
  let dir = extracted.get(spec.pkg);
287
545
  if (!dir) {
288
546
  try {
@@ -304,7 +562,10 @@ function vendor(): Problem[] {
304
562
  dir,
305
563
  '--strip-components=1',
306
564
  ]);
307
- if (spec.id === 'typescript') rebuildTypescript(dir, work);
565
+ if (spec.id === 'typescript' || spec.id === 'tsx') rebuildTypescript(dir, work, only);
566
+ if (spec.id === 'javascript') rebuildJavascript(dir, work);
567
+ if (spec.id === 'swift') rebuildSwift(dir, work);
568
+ if (spec.id === 'sql') rebuildSql(dir, work);
308
569
  extracted.set(spec.pkg, dir);
309
570
  } catch (err) {
310
571
  problems.push({
@@ -322,8 +583,13 @@ function vendor(): Problem[] {
322
583
  problems.push({ id: spec.id, detail: `${spec.pkg} ships no ${wasmName}` });
323
584
  } else {
324
585
  copyFileSync(wasmSrc, join(WASM_DIR, `${spec.id}.wasm`));
586
+ if (spec.id === 'lua') copyFileSync(join(dir, 'LICENSE.md'), join(WASM_DIR, 'lua.LICENSE'));
587
+ if (spec.id === 'javascript' || spec.id === 'tsx')
588
+ chmodSync(join(WASM_DIR, `${spec.id}.wasm`), 0o644);
325
589
  }
326
590
 
591
+ // A targeted JS scanner rebuild must retain our module-scope binding query additions.
592
+ if (only === 'javascript') continue;
327
593
  if (spec.tags === 'upstream') {
328
594
  const tagsSrc = join(dir, 'queries', 'tags.scm');
329
595
  if (!existsSync(tagsSrc)) {
@@ -55,6 +55,9 @@ const AST_LANGUAGE_ID_LIST = [
55
55
  'bash',
56
56
  'groovy',
57
57
  'powershell',
58
+ 'swift',
59
+ 'sql',
60
+ 'lua',
58
61
  ] as const;
59
62
 
60
63
  /** A language this build can AST-parse. Exactly the vendored set, no aspirations. */
@@ -123,6 +126,9 @@ export const QUERY_CHAIN: Readonly<Record<AstLanguageId, readonly AstLanguageId[
123
126
  bash: ['bash'],
124
127
  groovy: ['groovy'],
125
128
  powershell: ['powershell'],
129
+ swift: ['swift'],
130
+ sql: ['sql'],
131
+ lua: ['lua'],
126
132
  };
127
133
 
128
134
  /**
@@ -144,7 +150,7 @@ const EXTENSION_GRAMMARS: Readonly<Record<string, AstLanguageId>> = {
144
150
  *
145
151
  * Absences are deliberate answers, not gaps. `markdown` is missing because
146
152
  * headers already are a document's semantic unit and 10,483 production chunks
147
- * depend on `chunkMarkdown` keeping them; `kotlin`, `swift`, `dart` and friends
153
+ * depend on `chunkMarkdown` keeping them; `kotlin`, `dart` and friends
148
154
  * are missing because no loadable grammar is vendored yet. Both resolve to
149
155
  * `null` and the heuristic chunker takes over.
150
156
  */
@@ -166,6 +172,9 @@ const LANGUAGE_GRAMMARS: Readonly<Record<string, AstLanguageId>> = {
166
172
  shell: 'bash',
167
173
  groovy: 'groovy',
168
174
  powershell: 'powershell',
175
+ swift: 'swift',
176
+ sql: 'sql',
177
+ lua: 'lua',
169
178
  };
170
179
 
171
180
  /** A loaded grammar plus its compiled query. Compiled once, reused per file. */
@@ -563,6 +563,10 @@ export function languageForExtension(filePathOrExt: string): string {
563
563
  const isMigrationSqlAuthorityPath = (posixRelPath: string): boolean =>
564
564
  /^migrations\/.*\.sql$/.test(posixRelPath);
565
565
 
566
+ /** Content-only build authority, shared by scanner collection and server admission. */
567
+ export const XCODE_AUTHORITY_PATH = /(?:^|\/)[^/]+\.xcodeproj\/project\.pbxproj$|\.xcconfig$/;
568
+ export const isXcodeAuthorityPath = (path: string): boolean => XCODE_AUTHORITY_PATH.test(path);
569
+
566
570
  /**
567
571
  * Predicates for paths whose verbatim content is ALREADY shipped by
568
572
  * `collectAuthorityFiles` and that carry no additional value as embedded code
@@ -582,6 +586,7 @@ const isMigrationSqlAuthorityPath = (posixRelPath: string): boolean =>
582
586
  */
583
587
  const AUTHORITY_ONLY_PATH_PREDICATES: readonly ((posixRelPath: string) => boolean)[] = [
584
588
  isMigrationSqlAuthorityPath,
589
+ isXcodeAuthorityPath,
585
590
  ];
586
591
 
587
592
  /**
@@ -627,6 +632,7 @@ export const AUTHORITY_FILE_MATCHERS: Array<(relPath: string) => boolean> = [
627
632
  // `AUTHORITY_ONLY_PATH_PREDICATES` by reference, not by a copied regex, so
628
633
  // collection and the chunking exclusion cannot disagree.
629
634
  isMigrationSqlAuthorityPath,
635
+ isXcodeAuthorityPath,
630
636
  ];
631
637
 
632
638
  /**
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@ export {
26
26
  isFixturePath,
27
27
  isSecretFile,
28
28
  isAuthorityOnlyPath,
29
+ XCODE_AUTHORITY_PATH,
30
+ isXcodeAuthorityPath,
29
31
  isCursorRulePath,
30
32
  logAstCapabilityOnce,
31
33
  type CodeChunk,