@mnemonik/shared 7.142.0 → 7.144.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.
Files changed (66) hide show
  1. package/dist/ast/astChunker.d.ts.map +1 -1
  2. package/dist/ast/astChunker.js +6 -1
  3. package/dist/ast/astChunker.js.map +1 -1
  4. package/dist/ast/grammars.d.ts +4 -2
  5. package/dist/ast/grammars.d.ts.map +1 -1
  6. package/dist/ast/grammars.js +47 -1
  7. package/dist/ast/grammars.js.map +1 -1
  8. package/dist/codeScanner.d.ts +6 -0
  9. package/dist/codeScanner.d.ts.map +1 -1
  10. package/dist/codeScanner.js +12 -0
  11. package/dist/codeScanner.js.map +1 -1
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/sourceRedaction.d.ts.map +1 -1
  17. package/dist/sourceRedaction.js +1013 -21
  18. package/dist/sourceRedaction.js.map +1 -1
  19. package/dist/vueSource.d.ts +17 -0
  20. package/dist/vueSource.d.ts.map +1 -0
  21. package/dist/vueSource.js +63 -0
  22. package/dist/vueSource.js.map +1 -0
  23. package/package.json +3 -1
  24. package/queries/cmake.tags.scm +6 -0
  25. package/queries/nix.tags.scm +4 -0
  26. package/queries/objcpp.tags.scm +4 -0
  27. package/queries/sql.tags.scm +5 -0
  28. package/queries/swift.tags.scm +11 -0
  29. package/queries/terraform.tags.scm +8 -0
  30. package/scripts/jsx-bare-ampersand.patch +61 -0
  31. package/scripts/vendor-grammars.ts +731 -37
  32. package/src/ast/astChunker.ts +10 -1
  33. package/src/ast/grammars.ts +44 -2
  34. package/src/codeScanner.ts +12 -0
  35. package/src/index.ts +7 -0
  36. package/src/sourceRedaction.ts +1099 -21
  37. package/src/vueSource.ts +77 -0
  38. package/wasm/bash.wasm +0 -0
  39. package/wasm/cmake.LICENSE +21 -0
  40. package/wasm/cmake.tags.scm +6 -0
  41. package/wasm/cmake.wasm +0 -0
  42. package/wasm/cpp.LICENSE +21 -0
  43. package/wasm/cpp.wasm +0 -0
  44. package/wasm/groovy.LICENSE +25 -0
  45. package/wasm/groovy.wasm +0 -0
  46. package/wasm/javascript.wasm +0 -0
  47. package/wasm/lua.LICENSE +21 -0
  48. package/wasm/lua.tags.scm +36 -0
  49. package/wasm/lua.wasm +0 -0
  50. package/wasm/nix.LICENSE +21 -0
  51. package/wasm/nix.tags.scm +4 -0
  52. package/wasm/nix.wasm +0 -0
  53. package/wasm/objcpp.LICENSE +28 -0
  54. package/wasm/objcpp.tags.scm +4 -0
  55. package/wasm/objcpp.wasm +0 -0
  56. package/wasm/sql.LICENSE +21 -0
  57. package/wasm/sql.tags.scm +5 -0
  58. package/wasm/sql.wasm +0 -0
  59. package/wasm/swift.LICENSE +21 -0
  60. package/wasm/swift.tags.scm +11 -0
  61. package/wasm/swift.wasm +0 -0
  62. package/wasm/terraform.LICENSE +201 -0
  63. package/wasm/terraform.tags.scm +8 -0
  64. package/wasm/terraform.wasm +0 -0
  65. package/wasm/tsx.wasm +0 -0
  66. package/wasm/typescript.wasm +0 -0
@@ -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/source archive, or the pinned ObjC++ source repository. */
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
  }
@@ -71,22 +73,36 @@ interface GrammarSpec {
71
73
  * remove.
72
74
  *
73
75
  * Absent on purpose:
74
- * julia, objc, haskell - the three largest grammars that ALSO ship no tag
75
- * query, so each costs both its size (6.1/5.2/3.7 MB) and a hand-written
76
- * query. Dropped to keep the payload under 40 MB; still indexed via the
76
+ * julia, haskell - large grammars that ALSO ship no tag query, so each
77
+ * costs both its size (6.1/3.7 MB) and a hand-written query. Dropped to
78
+ * bound the payload; still indexed via the
77
79
  * allowlist, still chunked heuristically.
78
80
  * dart - its bundled .wasm (package v1.0.0, 2023) fails Language.load under
79
81
  * web-tree-sitter 0.26.11 with a dylink-metadata error: the artifact
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
87
89
  * unit; an AST would not improve on them.
88
90
  */
89
91
  const GRAMMARS: GrammarSpec[] = [
92
+ {
93
+ // Uy Ha 0.7.5, MIT; build the committed ABI-14 source without native dependencies.
94
+ id: 'cmake',
95
+ pkg: 'https://codeload.github.com/uyha/tree-sitter-cmake/tar.gz/58993af75218bc99a1f5a04c832a5937e7c422cb',
96
+ wasmFile: 'tree-sitter-cmake.wasm',
97
+ tags: 'ours',
98
+ },
99
+ {
100
+ // nix-community 0.3.0, MIT; use its committed generated ABI-15 parser unchanged.
101
+ id: 'nix',
102
+ pkg: 'https://codeload.github.com/nix-community/tree-sitter-nix/tar.gz/32c3ce98afdafc2ec9aedf0280a678750f76254c',
103
+ wasmFile: 'tree-sitter-nix.wasm',
104
+ tags: 'ours',
105
+ },
90
106
  {
91
107
  id: 'typescript',
92
108
  pkg: 'tree-sitter-typescript@0.23.2',
@@ -99,12 +115,20 @@ const GRAMMARS: GrammarSpec[] = [
99
115
  wasmFile: 'tree-sitter-tsx.wasm',
100
116
  tags: 'inherited',
101
117
  },
102
- { id: 'javascript', pkg: 'tree-sitter-javascript', tags: 'upstream' },
118
+ { id: 'javascript', pkg: 'tree-sitter-javascript@0.25.0', tags: 'upstream' },
103
119
  { id: 'python', pkg: 'tree-sitter-python', tags: 'upstream' },
104
120
  { id: 'go', pkg: 'tree-sitter-go', tags: 'upstream' },
105
121
  { id: 'java', pkg: 'tree-sitter-java', tags: 'upstream' },
106
122
  { id: 'c', pkg: 'tree-sitter-c', tags: 'upstream' },
107
- { id: 'cpp', pkg: 'tree-sitter-cpp', tags: 'upstream' },
123
+ // MIT 0.23.4, gitHead f41e1a044c8a84ea9fa8577fdd2eab92ec96de02.
124
+ { id: 'cpp', pkg: 'tree-sitter-cpp@0.23.4', tags: 'upstream' },
125
+ {
126
+ // MIT Objective-C++ 0.1.0; committed ABI-15 parser, not the pure Objective-C npm grammar.
127
+ id: 'objcpp',
128
+ pkg: 'https://tangled.org/dunkirk.sh/dots',
129
+ wasmFile: 'tree-sitter-objcpp.wasm',
130
+ tags: 'ours',
131
+ },
108
132
  {
109
133
  id: 'c-sharp',
110
134
  pkg: 'tree-sitter-c-sharp',
@@ -117,9 +141,39 @@ const GRAMMARS: GrammarSpec[] = [
117
141
  { id: 'scala', pkg: 'tree-sitter-scala', tags: 'upstream' },
118
142
  { id: 'elixir', pkg: 'tree-sitter-elixir', tags: 'upstream' },
119
143
  { id: 'solidity', pkg: 'tree-sitter-solidity', tags: 'upstream' },
120
- { id: 'bash', pkg: 'tree-sitter-bash', tags: 'ours' },
121
- { id: 'groovy', pkg: 'tree-sitter-groovy', tags: 'ours' },
144
+ // MIT 0.25.1, gitHead 801326684a26ffc4e749bb016c50c6c30bdfa345.
145
+ { id: 'bash', pkg: 'tree-sitter-bash@0.25.1', tags: 'ours' },
146
+ {
147
+ // Upstream MIT 0.4.1, gitHead 816840c592ab973500ae9750763c707b447e7fef; shipped ABI-15 WASM.
148
+ id: 'lua',
149
+ pkg: '@tree-sitter-grammars/tree-sitter-lua@0.4.1',
150
+ wasmFile: 'tree-sitter-lua.wasm',
151
+ tags: 'upstream',
152
+ },
153
+ {
154
+ // Apache-2.0 v1.2.0, gitHead fad991865fee927dd1de5e172fb3f08ac674d914; packaged ABI-14 WASM.
155
+ id: 'terraform',
156
+ pkg: '@tree-sitter-grammars/tree-sitter-hcl@1.2.0',
157
+ wasmFile: 'tree-sitter-terraform.wasm',
158
+ tags: 'ours',
159
+ },
160
+ // MIT 0.1.2, gitHead 70efb0b9b50f95bcbd89dcfd42b275e0304e10cf.
161
+ { id: 'groovy', pkg: 'tree-sitter-groovy@0.1.2', tags: 'ours' },
122
162
  { id: 'powershell', pkg: 'tree-sitter-powershell', tags: 'ours' },
163
+ {
164
+ id: 'swift',
165
+ // Upstream 0.7.3, MIT. Pin source, not an opaque workflow WASM.
166
+ pkg: 'https://codeload.github.com/alex-pinkus/tree-sitter-swift/tar.gz/b8b22bffbb3441780e6471665bacfb263741c86a',
167
+ wasmFile: 'tree-sitter-swift.wasm',
168
+ tags: 'ours',
169
+ },
170
+ {
171
+ id: 'sql',
172
+ // @derekstride/tree-sitter-sql 0.3.11 gitHead, MIT; source includes the external scanner.
173
+ pkg: 'https://codeload.github.com/derekstride/tree-sitter-sql/tar.gz/7b51ecda191d36b92f5a90a8d1bc3faef1c7b8b8',
174
+ wasmFile: 'tree-sitter-sql.wasm',
175
+ tags: 'ours',
176
+ },
123
177
  ];
124
178
 
125
179
  /**
@@ -229,7 +283,7 @@ async function checkOnly(): Promise<Problem[]> {
229
283
  * Source: https://github.com/tree-sitter/tree-sitter-typescript/tree/v0.23.2
230
284
  * MIT; CLI 0.24.4 uses emscripten/emsdk:3.1.64 for the WASM build.
231
285
  */
232
- function rebuildTypescript(dir: string, work: string): void {
286
+ function rebuildTypescript(dir: string, work: string, only?: string): void {
233
287
  const grammarPath = join(dir, 'common', 'define-grammar.js');
234
288
  let grammar = readFileSync(grammarPath, 'utf8');
235
289
  for (const [before, after] of [
@@ -246,7 +300,37 @@ function rebuildTypescript(dir: string, work: string): void {
246
300
  if (!grammar.includes(before)) throw new Error('TypeScript grammar patch no longer applies');
247
301
  grammar = grammar.replace(before, after);
248
302
  }
303
+ // Import types are primary types, so postfix [] applies without changing their expression aliases.
304
+ const importTypes = ` prec(-1, alias($._type_query_member_expression_in_type_annotation, $.member_expression)),
305
+ prec(-1, alias($._type_query_call_expression_in_type_annotation, $.call_expression)),
306
+ `;
307
+ const exportSpecifier = ` export_specifier: (_, previous) => seq(
308
+ optional(choice('type', 'typeof')),
309
+ previous,
310
+ ),`;
311
+ if (
312
+ !grammar.includes(importTypes) ||
313
+ !grammar.includes(exportSpecifier) ||
314
+ !grammar.includes(' primary_type: $ => choice(')
315
+ )
316
+ throw new Error('TypeScript import-type/export patch no longer applies');
317
+ grammar = grammar
318
+ .replace(importTypes, '')
319
+ .replace(
320
+ ' primary_type: $ => choice(',
321
+ ' primary_type: $ => choice(\n' + importTypes.trimEnd()
322
+ );
323
+ grammar = grammar.replace(
324
+ exportSpecifier,
325
+ ` export_specifier: $ => seq(
326
+ optional(choice('type', 'typeof')),
327
+ field('name', choice($._module_export_name, alias('type', $.identifier))),
328
+ optional(seq('as', field('alias', $._module_export_name))),
329
+ ),`
330
+ );
249
331
  writeFileSync(grammarPath, grammar);
332
+ // Retain our type/enum declaration captures when the vendor loop copies the query.
333
+ copyFileSync(join(WASM_DIR, 'typescript.tags.scm'), join(dir, 'queries', 'tags.scm'));
250
334
  const tools = join(work, 'typescript-build');
251
335
  execFileSync('npm', [
252
336
  'install',
@@ -260,6 +344,12 @@ function rebuildTypescript(dir: string, work: string): void {
260
344
  ]);
261
345
  const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
262
346
  for (const dialect of ['typescript', 'tsx']) {
347
+ if (only && only !== dialect) continue;
348
+ // Apply the JSX scanner and attribute repairs only to TSX.
349
+ if (dialect === 'tsx') {
350
+ patchJsxScanner(join(dir, 'common', 'scanner.h'));
351
+ patchJsxAttributes(join(tools, 'node_modules', 'tree-sitter-javascript', 'grammar.js'));
352
+ }
263
353
  const options = {
264
354
  cwd: join(dir, dialect),
265
355
  env: { ...process.env, NODE_PATH: join(tools, 'node_modules') },
@@ -270,10 +360,586 @@ function rebuildTypescript(dir: string, work: string): void {
270
360
  ['build', '--wasm', '--docker', '--output', join(dir, `tree-sitter-${dialect}.wasm`)],
271
361
  options
272
362
  );
363
+ chmodSync(join(dir, `tree-sitter-${dialect}.wasm`), 0o644);
364
+ }
365
+ }
366
+
367
+ /** Upstream f8637eb: preserve entity boundaries while accepting bare JSX ampersands. */
368
+ function patchJsxScanner(scannerPath: string): void {
369
+ execFileSync('patch', ['--batch', scannerPath], {
370
+ input: readFileSync(new URL('./jsx-bare-ampersand.patch', import.meta.url)),
371
+ });
372
+ }
373
+
374
+ /** Bare attribute ampersands are fragments; complete entity tokens retain precedence. */
375
+ function patchJsxAttributes(grammarPath: string): void {
376
+ let grammar = readFileSync(grammarPath, 'utf8');
377
+ for (const [before, after] of [
378
+ [
379
+ 'alias($.unescaped_double_jsx_string_fragment, $.string_fragment),',
380
+ "alias($.unescaped_double_jsx_string_fragment, $.string_fragment),\n alias(token.immediate(prec(-1, '&')), $.string_fragment),",
381
+ ],
382
+ [
383
+ 'alias($.unescaped_single_jsx_string_fragment, $.string_fragment),',
384
+ "alias($.unescaped_single_jsx_string_fragment, $.string_fragment),\n alias(token.immediate(prec(-1, '&')), $.string_fragment),",
385
+ ],
386
+ ['/([^"&]|&[^#A-Za-z])+/', '/[^"&]+/'],
387
+ ["/([^'&]|&[^#A-Za-z])+/", "/[^'&]+/"],
388
+ ] as const) {
389
+ if (!grammar.includes(before)) throw new Error('JSX attribute patch no longer applies');
390
+ grammar = grammar.replace(before, after);
391
+ }
392
+ writeFileSync(grammarPath, grammar);
393
+ }
394
+
395
+ /** 0.25.0's reserved-word DSL needs generator 0.25.8; keep the existing WASM build tool. */
396
+ function rebuildJavascript(dir: string, work: string): void {
397
+ patchJsxScanner(join(dir, 'src', 'scanner.c'));
398
+ patchJsxAttributes(join(dir, 'grammar.js'));
399
+ const generator = join(work, 'javascript-generator');
400
+ execFileSync('npm', [
401
+ 'install',
402
+ '--prefix',
403
+ generator,
404
+ '--no-audit',
405
+ '--no-fund',
406
+ '--no-package-lock',
407
+ 'tree-sitter-cli@0.25.8',
408
+ ]);
409
+ execFileSync(join(generator, 'node_modules', '.bin', 'tree-sitter'), ['generate'], { cwd: dir });
410
+ const tools = join(work, 'javascript-build');
411
+ execFileSync('npm', [
412
+ 'install',
413
+ '--prefix',
414
+ tools,
415
+ '--no-audit',
416
+ '--no-fund',
417
+ '--no-package-lock',
418
+ 'tree-sitter-cli@0.24.4',
419
+ ]);
420
+ // CLI 0.24.4 pins emscripten/emsdk:3.1.64 for --docker.
421
+ execFileSync(
422
+ join(tools, 'node_modules', '.bin', 'tree-sitter'),
423
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-javascript.wasm')],
424
+ { cwd: dir }
425
+ );
426
+ }
427
+
428
+ /** Same CLI/emsdk as the repaired TS grammars; no native binding or grammar dependency. */
429
+ function rebuildSwift(dir: string, work: string): void {
430
+ // A double optional marker must be adjacent to its type. The external ?? token
431
+ // skips whitespace, swallowing `as? Int ?? 0` into optional_type and rejecting 0.
432
+ const grammarPath = join(dir, 'grammar.js');
433
+ const grammar = readFileSync(grammarPath, 'utf8');
434
+ const before = ' alias($._nil_coalescing_operator, "??")';
435
+ if (!grammar.includes(before)) throw new Error('Swift optional-marker patch no longer applies');
436
+ writeFileSync(grammarPath, grammar.replace(before, ' token.immediate("??")'));
437
+ const scannerPath = join(dir, 'src', 'scanner.c');
438
+ const scanner = readFileSync(scannerPath, 'utf8');
439
+ const continuation =
440
+ ' // Let\'s consume operators that can live after a "semicolon" style newline.';
441
+ if (!scanner.includes(continuation))
442
+ throw new Error('Swift multiplication patch no longer applies');
443
+ writeFileSync(
444
+ scannerPath,
445
+ scanner.replace(
446
+ continuation,
447
+ ` // A whitespace-delimited multiplication operator continues the preceding expression.
448
+ // Keep mark_end before '*' so a non-binary spelling can still emit the newline.
449
+ if (ws_directive == CONTINUE_PARSING_TOKEN_FOUND && lookahead == '*') {
450
+ lexer->advance(lexer, false);
451
+ if (iswspace(lexer->lookahead)) return STOP_PARSING_NOTHING_FOUND;
452
+ }
453
+
454
+ ${continuation}`
455
+ )
456
+ );
457
+ const tools = join(work, 'swift-build');
458
+ execFileSync('npm', [
459
+ 'install',
460
+ '--prefix',
461
+ tools,
462
+ '--no-audit',
463
+ '--no-fund',
464
+ '--no-package-lock',
465
+ 'tree-sitter-cli@0.24.4',
466
+ ]);
467
+ const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
468
+ execFileSync(cli, ['generate'], { cwd: dir });
469
+ // CLI 0.24.4 pins emscripten/emsdk:3.1.64 for --docker.
470
+ execFileSync(
471
+ cli,
472
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-swift.wasm')],
473
+ { cwd: dir }
474
+ );
475
+ copyFileSync(join(dir, 'LICENSE'), join(WASM_DIR, 'swift.LICENSE'));
476
+ }
477
+
478
+ /** Build committed generated C without installing the dots repository or its grammar ancestors. */
479
+ function vendorObjcpp(work: string, repository: string): string {
480
+ const root = join(work, 'objcpp-source');
481
+ mkdirSync(root);
482
+ const git = (args: string[]): void => {
483
+ execFileSync('git', args, { cwd: root });
484
+ };
485
+ git(['init', '--quiet']);
486
+ git(['remote', 'add', 'origin', repository]);
487
+ git(['config', 'remote.origin.promisor', 'true']);
488
+ git(['config', 'remote.origin.partialclonefilter', 'blob:none']);
489
+ git(['sparse-checkout', 'set', '--no-cone', '/LICENSE.md', '/packages/tree-sitter-objcpp/']);
490
+ git([
491
+ 'fetch',
492
+ '--quiet',
493
+ '--depth=1',
494
+ '--filter=blob:none',
495
+ 'origin',
496
+ '8c861a98d84c6325a0300ec88dfa334aa6ab150e',
497
+ ]);
498
+ git(['checkout', '--quiet', '--detach', 'FETCH_HEAD']);
499
+ const dir = join(root, 'packages', 'tree-sitter-objcpp');
500
+ const tools = join(work, 'objcpp-build');
501
+ execFileSync('npm', [
502
+ 'install',
503
+ '--prefix',
504
+ tools,
505
+ '--no-audit',
506
+ '--no-fund',
507
+ '--no-package-lock',
508
+ 'tree-sitter-cli@0.24.4',
509
+ ]);
510
+ // Upstream generated this ABI-15 parser with CLI 0.25.10; 0.24.4 builds it with emsdk 3.1.64.
511
+ execFileSync(
512
+ join(tools, 'node_modules', '.bin', 'tree-sitter'),
513
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-objcpp.wasm')],
514
+ { cwd: dir }
515
+ );
516
+ writeFileSync(
517
+ join(WASM_DIR, 'objcpp.LICENSE'),
518
+ readFileSync(join(root, 'LICENSE.md'), 'utf8').replace(/[\u201c\u201d]/g, '"') +
519
+ '\nInherited C/C++ parser and scanner portions: Copyright (c) 2014 Max Brunsfeld.\nThe MIT grant and conditions above apply to these portions too.\n'
520
+ );
521
+ return dir;
522
+ }
523
+
524
+ /** Upstream package.json pins CLI 0.24.7. SQL function bodies only, not general PL/pgSQL. */
525
+ function rebuildSql(dir: string, work: string): void {
526
+ const grammarPath = join(dir, 'grammar.js');
527
+ let grammar = readFileSync(grammarPath, 'utf8');
528
+ const patch = (before: string, after: string): void => {
529
+ if (!grammar.includes(before)) throw new Error(`SQL patch no longer applies: ${before}`);
530
+ grammar = grammar.replace(before, after);
531
+ };
532
+ // Bounded repairs demonstrated by migrations 289/279/283 and the escaped-name regression.
533
+ const constraint = ` _constraint_literal: $ => seq(
534
+ $.keyword_constraint,
535
+ field('name', $.identifier),
536
+ choice(`;
537
+ patch(constraint, constraint + '\n $._key_constraint,');
538
+ patch('_double_quote_string: _ => /"[^"]*"/', '_double_quote_string: _ => /"([^"]|"")*"/');
539
+ patch(
540
+ 'wrapped_in_parenthesis($.binary_expression)\n ),\n\n _default_expression',
541
+ 'wrapped_in_parenthesis($._expression)\n ),\n\n _default_expression'
542
+ );
543
+ patch(
544
+ 'field("custom_type", $.object_reference)',
545
+ 'seq(field("custom_type", $.object_reference), optional(wrapped_in_parenthesis($._natural_number)))'
546
+ );
547
+ patch(
548
+ ` $.keyword_conflict,
549
+ seq(`,
550
+ ` $.keyword_conflict,
551
+ optional($._column_list),
552
+ seq(`
553
+ );
554
+ patch(
555
+ ` $.keyword_brin
556
+ ),`,
557
+ ` $.keyword_brin,
558
+ $.identifier
559
+ ),`
560
+ );
561
+ patch(
562
+ ` $.index_fields
563
+ ),`,
564
+ ` $.index_fields
565
+ ),
566
+ optional($.storage_parameters),`
567
+ );
568
+ patch(
569
+ ` $.drop_function,
570
+ ),`,
571
+ ` $.drop_function,
572
+ $.drop_trigger,
573
+ ),`
574
+ );
575
+ patch(
576
+ ' drop_table: $ => seq(',
577
+ ` drop_trigger: $ => seq(
578
+ $.keyword_drop, $.keyword_trigger, optional($._if_exists),
579
+ $.identifier, $.keyword_on, $.object_reference, optional($._drop_behavior),
580
+ ),
581
+
582
+ drop_table: $ => seq(`
583
+ );
584
+ // Only the policy clauses and RLS action observed in the four fixed PostgreSQL migrations.
585
+ patch(' $.create_trigger,', ' $.create_trigger,\n $.create_policy,');
586
+ patch(
587
+ ' _alter_specifications: $ => choice(',
588
+ ` _alter_specifications: $ => choice(
589
+ seq(make_keyword("enable"), $.keyword_row, $.keyword_level, $.keyword_security),`
590
+ );
591
+ patch(
592
+ ' create_table: $ => prec.left(',
593
+ ` policy_relation: $ => $.object_reference,
594
+ create_policy: $ => seq(
595
+ $.keyword_create, make_keyword("policy"), field('name', $.identifier),
596
+ $.keyword_on, alias($.policy_relation, $.relation),
597
+ optional(seq($.keyword_for, choice($.keyword_select, $.keyword_insert, $.keyword_update, $.keyword_delete))),
598
+ optional(seq($.keyword_to, $.identifier)),
599
+ optional(seq($.keyword_using, wrapped_in_parenthesis($._expression))),
600
+ optional(seq($.keyword_with, $.keyword_check, wrapped_in_parenthesis($._expression))),
601
+ ),
602
+ create_table: $ => prec.left(`
603
+ );
604
+ // Dollar-quoted procedural code is one literal, never a parsed SQL statement list.
605
+ patch(' $.reset_statement,', ' $.reset_statement,\n $.do_statement,');
606
+ patch(
607
+ ' _cte: $ => seq(',
608
+ ` do_statement: $ => seq($.keyword_do, alias($._dollar_quoted_string, $.literal)),
609
+
610
+ _cte: $ => seq(`
611
+ );
612
+ // Keep both parses until NOT NULL (constraint) or NOT LIKE (expression) is known.
613
+ // Match _expression's precedence so it cannot discard the completed default first.
614
+ patch(
615
+ ' _inner_default_expression: $ => choice(',
616
+ ' _inner_default_expression: $ => prec(1, choice('
617
+ );
618
+ patch(
619
+ 'alias($.implicit_cast, $.cast),\n ),\n\n constraints:',
620
+ 'alias($.implicit_cast, $.cast),\n )),\n\n constraints:'
621
+ );
622
+ patch(
623
+ ' conflicts: $ => [',
624
+ ' conflicts: $ => [\n [$._expression, $._inner_default_expression],\n [$.create_function],'
625
+ );
626
+ const functionStart = grammar.indexOf(' create_function: $ => seq(');
627
+ const functionEnd = grammar.indexOf(' _function_return:', functionStart);
628
+ if (functionStart < 0 || functionEnd < 0) throw new Error('SQL function patch no longer applies');
629
+ const original = grammar.slice(functionStart, functionEnd).trimEnd();
630
+ const languageOption = ' $.function_language,\n';
631
+ const optionsStart = original.indexOf(' repeat(');
632
+ const optionsEnd = original.indexOf(' // ensure');
633
+ const options = original.slice(optionsStart, optionsEnd).replace(languageOption, '');
634
+ const procedural =
635
+ original.slice(0, optionsStart) +
636
+ options +
637
+ ' alias($.plpgsql_function_language, $.function_language),\n' +
638
+ options +
639
+ ' alias($.opaque_function_body, $.function_body),\n' +
640
+ options +
641
+ ' )';
642
+ patch(
643
+ original,
644
+ procedural.replace('create_function: $ => seq(', 'create_function: $ => choice(prec(1, seq(') +
645
+ '),\n' +
646
+ original.slice(original.indexOf('seq('), -1) +
647
+ '),'
648
+ );
649
+ patch(
650
+ ' $._dollar_quoted_string,\n ],',
651
+ ' $._dollar_quoted_string,\n $._trailing_plpgsql_body,\n ],'
652
+ );
653
+ // Named rules retain complete body/language ranges; aliasing a sequence only names its first token.
654
+ patch(
655
+ ' function_body: $ => choice(',
656
+ ` opaque_function_body: $ => seq(
657
+ $.keyword_as,
658
+ alias(choice($._dollar_quoted_string, $._single_quote_string, $._double_quote_string), $.literal),
659
+ ),
660
+ plpgsql_function_language: $ => seq(
661
+ $.keyword_language, alias(make_keyword("plpgsql"), $.identifier),
662
+ ),
663
+ function_body: $ => choice(
664
+ seq(
665
+ $.keyword_as,
666
+ alias($._dollar_quoted_string_start_tag, $.dollar_quote),
667
+ alias($._trailing_plpgsql_body, $.literal),
668
+ alias($._dollar_quoted_string_end_tag, $.dollar_quote),
669
+ ),`
670
+ );
671
+ const languageStart = grammar.indexOf(' function_language: $ => seq(');
672
+ const languageEnd = grammar.indexOf(' function_volatility:', languageStart);
673
+ if (languageStart < 0 || languageEnd < 0) throw new Error('SQL language patch no longer applies');
674
+ const language = grammar.slice(languageStart, languageEnd);
675
+ patch(
676
+ language,
677
+ language.replace(
678
+ ' $.identifier',
679
+ ` choice(
680
+ $.identifier,
681
+ alias(token(prec(1, seq("'", make_keyword("plpgsql"), "'"))), $.identifier),
682
+ )`
683
+ )
684
+ );
685
+ writeFileSync(grammarPath, grammar);
686
+ const scannerPath = join(dir, 'src', 'scanner.c');
687
+ let scanner = readFileSync(scannerPath, 'utf8');
688
+ const token = ' DOLLAR_QUOTED_STRING\n';
689
+ const start =
690
+ ' if (valid_symbols[DOLLAR_QUOTED_STRING_START_TAG] && state->start_tag == NULL) {';
691
+ if (!scanner.includes(token) || !scanner.includes(start))
692
+ throw new Error('SQL procedural scanner patch no longer applies');
693
+ scanner = scanner.replace(token, ' DOLLAR_QUOTED_STRING,\n TRAILING_PLPGSQL_BODY\n');
694
+ scanner = scanner.replace(
695
+ start,
696
+ () =>
697
+ ` if (valid_symbols[TRAILING_PLPGSQL_BODY] && state->start_tag != NULL) {
698
+ bool consumed = false;
699
+ for (;;) {
700
+ if (lexer->eof(lexer)) return false;
701
+ if (lexer->lookahead != '$') {
702
+ lexer->advance(lexer, false);
703
+ consumed = true;
704
+ continue;
705
+ }
706
+ lexer->mark_end(lexer);
707
+ char *tag = scan_dollar_string_tag(lexer);
708
+ bool closing = tag != NULL && strcmp(tag, state->start_tag) == 0;
709
+ free(tag);
710
+ if (closing) break;
711
+ consumed = true;
712
+ }
713
+ if (!consumed) return false;
714
+ // Look ahead without moving the marked body end; SQL bodies keep their normal AST.
715
+ while (iswspace(lexer->lookahead)) lexer->advance(lexer, false);
716
+ const char *keyword = "language";
717
+ for (unsigned i = 0; keyword[i]; i++) {
718
+ if (towlower(lexer->lookahead) != keyword[i]) return false;
719
+ lexer->advance(lexer, false);
720
+ }
721
+ while (iswspace(lexer->lookahead)) lexer->advance(lexer, false);
722
+ bool quoted = lexer->lookahead == 39;
723
+ if (quoted) lexer->advance(lexer, false);
724
+ const char *language = "plpgsql";
725
+ for (unsigned i = 0; language[i]; i++) {
726
+ if (towlower(lexer->lookahead) != language[i]) return false;
727
+ lexer->advance(lexer, false);
728
+ }
729
+ if (quoted ? lexer->lookahead != 39 : (iswalnum(lexer->lookahead) || lexer->lookahead == '_' || lexer->lookahead == '$')) return false;
730
+ lexer->result_symbol = TRAILING_PLPGSQL_BODY;
731
+ return true;
273
732
  }
733
+
734
+ ` + start
735
+ );
736
+ writeFileSync(scannerPath, scanner);
737
+ const tools = join(work, 'sql-build');
738
+ execFileSync('npm', [
739
+ 'install',
740
+ '--prefix',
741
+ tools,
742
+ '--no-audit',
743
+ '--no-fund',
744
+ '--no-package-lock',
745
+ 'tree-sitter-cli@0.24.7',
746
+ ]);
747
+ const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
748
+ execFileSync(cli, ['generate'], { cwd: dir });
749
+ execFileSync(
750
+ cli,
751
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-sql.wasm')],
752
+ { cwd: dir }
753
+ );
754
+ copyFileSync(join(dir, 'LICENSE'), join(WASM_DIR, 'sql.LICENSE'));
755
+ }
756
+
757
+ /** Permit existing dotted property expressions as command-call arguments. */
758
+ function rebuildGroovy(dir: string, work: string): void {
759
+ const path = join(dir, 'grammar.js');
760
+ let source = readFileSync(path, 'utf8');
761
+ const before =
762
+ ' _juxt_argument_list: $ => commaSep1(choice($._literal, $.map_item, $.identifier)),';
763
+ if (!source.includes(before)) throw new Error('Groovy argument patch no longer applies');
764
+ source = source.replace(before, before.replace('$.identifier)', '$.identifier, $.field_access)'));
765
+ const conflicts = ' conflicts: ($, original) => original.concat([';
766
+ if (!source.includes(conflicts)) throw new Error('Groovy conflict patch no longer applies');
767
+ source = source.replace(
768
+ conflicts,
769
+ conflicts +
770
+ `
771
+ [$.method_invocation, $._juxt_argument_list],
772
+ [$.generic_type, $._juxt_argument_list],
773
+ [$.primary_expression, $._unannotated_type, $._juxt_argument_list],
774
+ [$._unannotated_type, $._juxt_argument_list],
775
+ [$.primary_expression, $._juxt_argument_list],`
776
+ );
777
+ writeFileSync(path, source);
778
+ const tools = join(work, 'groovy-build');
779
+ execFileSync('npm', [
780
+ 'install',
781
+ '--prefix',
782
+ tools,
783
+ '--no-audit',
784
+ '--no-fund',
785
+ '--no-package-lock',
786
+ 'tree-sitter-cli@0.24.4',
787
+ 'tree-sitter-java@0.23.4',
788
+ ]);
789
+ const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
790
+ const options = { cwd: dir, env: { ...process.env, NODE_PATH: join(tools, 'node_modules') } };
791
+ execFileSync(cli, ['generate'], options);
792
+ execFileSync(
793
+ cli,
794
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-groovy.wasm')],
795
+ options
796
+ );
797
+ // Upstream publishes its MIT declaration in package.json/grammar.js, with no LICENSE file.
798
+ writeFileSync(
799
+ join(WASM_DIR, 'groovy.LICENSE'),
800
+ 'tree-sitter-groovy 0.1.2, by Amaan Qureshi <amaanq12@gmail.com>.\n' +
801
+ 'Upstream package.json and grammar.js declare MIT. Source: https://github.com/amaanq/tree-sitter-groovy/tree/70efb0b9b50f95bcbd89dcfd42b275e0304e10cf\n\n' +
802
+ 'Inherited tree-sitter-java 0.23.4 (MIT):\n' +
803
+ readFileSync(join(tools, 'node_modules', 'tree-sitter-java', 'LICENSE'), 'utf8')
804
+ );
805
+ }
806
+
807
+ /** Build the official committed Nix parser without installing its native Node binding. */
808
+ function rebuildNixOrCmake(dir: string, work: string, id: 'nix' | 'cmake'): void {
809
+ const tools = join(work, `${id}-build`);
810
+ execFileSync('npm', [
811
+ 'install',
812
+ '--prefix',
813
+ tools,
814
+ '--no-audit',
815
+ '--no-fund',
816
+ '--no-package-lock',
817
+ 'tree-sitter-cli@0.24.4',
818
+ ]);
819
+ // Build the pinned generated source; CLI 0.24.4 pins emscripten/emsdk:3.1.64.
820
+ execFileSync(
821
+ join(tools, 'node_modules', '.bin', 'tree-sitter'),
822
+ ['build', '--wasm', '--docker', '--output', join(dir, `tree-sitter-${id}.wasm`)],
823
+ { cwd: dir }
824
+ );
825
+ copyFileSync(join(dir, 'LICENSE'), join(WASM_DIR, `${id}.LICENSE`));
826
+ }
827
+
828
+ /** The published grammar JSON freezes its C ancestor; add only the two JNI modifier spellings. */
829
+ function rebuildCpp(dir: string, work: string): void {
830
+ const path = join(dir, 'src', 'grammar.json');
831
+ const grammar = JSON.parse(readFileSync(path, 'utf8')) as {
832
+ rules: Record<string, { type: string; members: unknown[] }>;
833
+ };
834
+ for (const [name, value] of [
835
+ ['type_qualifier', 'JNIEXPORT'],
836
+ ['ms_call_modifier', 'JNICALL'],
837
+ ] as const) {
838
+ const rule = grammar.rules[name];
839
+ if (!rule || rule.type !== 'CHOICE' || !Array.isArray(rule.members))
840
+ throw new Error(`C++ JNI patch no longer applies: ${name}`);
841
+ rule.members.push({ type: 'STRING', value });
842
+ }
843
+ writeFileSync(path, JSON.stringify(grammar));
844
+ const tools = join(work, 'cpp-build');
845
+ execFileSync('npm', [
846
+ 'install',
847
+ '--prefix',
848
+ tools,
849
+ '--no-audit',
850
+ '--no-fund',
851
+ '--no-package-lock',
852
+ 'tree-sitter-cli@0.24.4',
853
+ ]);
854
+ const cli = join(tools, 'node_modules', '.bin', 'tree-sitter');
855
+ execFileSync(cli, ['generate', 'src/grammar.json'], { cwd: dir });
856
+ execFileSync(
857
+ cli,
858
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-cpp.wasm')],
859
+ { cwd: dir }
860
+ );
861
+ copyFileSync(join(dir, 'LICENSE'), join(WASM_DIR, 'cpp.LICENSE'));
862
+ }
863
+
864
+ /** A radix value may use $name as well as ${name}; keep the source and expansion node intact. */
865
+ function rebuildBash(dir: string, work: string): void {
866
+ const path = join(dir, 'grammar.js');
867
+ const grammar = readFileSync(path, 'utf8');
868
+ const before = 'seq(/-?(0x)?[0-9]+#/, choice($.expansion, $.command_substitution))';
869
+ if (!grammar.includes(before)) throw new Error('Bash radix patch no longer applies');
870
+ writeFileSync(
871
+ path,
872
+ grammar.replace(
873
+ before,
874
+ 'seq(/-?(0x)?[0-9]+#/, choice($.simple_expansion, $.expansion, $.command_substitution))'
875
+ )
876
+ );
877
+ const scannerPath = join(dir, 'src', 'scanner.c');
878
+ const scanner = readFileSync(scannerPath, 'utf8');
879
+ const variableScan = ` return false;
880
+ }
881
+
882
+ if (valid_symbols[BARE_DOLLAR] && !in_error_recovery(valid_symbols) && scan_bare_dollar(lexer)) {`;
883
+ if (!scanner.includes(variableScan))
884
+ throw new Error('Bash expansion-word patch no longer applies');
885
+ const parenStart = scanner.indexOf(
886
+ " if (lexer->lookahead == '(' && !(advanced_once || advance_once_space)) {"
887
+ );
888
+ const parenEnd = scanner.indexOf(
889
+ String.raw` if (lexer->lookahead == '\'') {`,
890
+ parenStart
891
+ );
892
+ const concatParens = "lexer->lookahead == ')' || lexer->lookahead == '('";
893
+ if (parenStart < 0 || parenEnd <= parenStart || !scanner.includes(concatParens))
894
+ throw new Error('Bash literal-parenthesis patch no longer applies');
895
+ // The variable-name scan consumes a default's prefix but rejects | before reaching expansion_word.
896
+ // Inside ${...}, parentheses are word pieces around expansions, not shell groups.
897
+ writeFileSync(
898
+ scannerPath,
899
+ (scanner.slice(0, parenStart) + scanner.slice(parenEnd))
900
+ .replace(
901
+ variableScan,
902
+ " if (valid_symbols[EXPANSION_WORD] && lexer->lookahead == '|') goto expansion_word;\n\n" +
903
+ variableScan
904
+ )
905
+ .replace(
906
+ concatParens,
907
+ "((lexer->lookahead == ')' || lexer->lookahead == '(') && !valid_symbols[CLOSING_BRACE])"
908
+ )
909
+ );
910
+ const generator = join(work, 'bash-generator');
911
+ execFileSync('npm', [
912
+ 'install',
913
+ '--prefix',
914
+ generator,
915
+ '--no-audit',
916
+ '--no-fund',
917
+ '--no-package-lock',
918
+ 'tree-sitter-cli@0.25.8',
919
+ ]);
920
+ execFileSync(join(generator, 'node_modules', '.bin', 'tree-sitter'), ['generate'], { cwd: dir });
921
+ const tools = join(work, 'bash-build');
922
+ execFileSync('npm', [
923
+ 'install',
924
+ '--prefix',
925
+ tools,
926
+ '--no-audit',
927
+ '--no-fund',
928
+ '--no-package-lock',
929
+ 'tree-sitter-cli@0.24.4',
930
+ ]);
931
+ // CLI 0.24.4 pins emscripten/emsdk:3.1.64 for --docker.
932
+ execFileSync(
933
+ join(tools, 'node_modules', '.bin', 'tree-sitter'),
934
+ ['build', '--wasm', '--docker', '--output', join(dir, 'tree-sitter-bash.wasm')],
935
+ { cwd: dir }
936
+ );
274
937
  }
275
938
 
276
939
  function vendor(): Problem[] {
940
+ const only = process.argv.find((arg) => arg.startsWith('--only='))?.slice(7);
941
+ if (only && !GRAMMARS.some((spec) => spec.id === only))
942
+ throw new Error(`Unknown grammar: ${only}`);
277
943
  mkdirSync(WASM_DIR, { recursive: true });
278
944
  const work = mkdtempSync(join(tmpdir(), 'ts-grammars-'));
279
945
  const problems: Problem[] = [];
@@ -283,28 +949,40 @@ function vendor(): Problem[] {
283
949
 
284
950
  try {
285
951
  for (const spec of GRAMMARS) {
952
+ if (only && spec.id !== only) continue;
286
953
  let dir = extracted.get(spec.pkg);
287
954
  if (!dir) {
288
955
  try {
289
- const out = execFileSync(
290
- 'npm',
291
- ['pack', spec.pkg, '--silent', '--pack-destination', work],
292
- {
293
- cwd: work,
294
- encoding: 'utf8',
295
- }
296
- );
297
- const tarball = out.trim().split('\n').pop() ?? '';
298
- dir = join(work, spec.pkg.replace(/[^a-z0-9-]/gi, '_'));
299
- mkdirSync(dir, { recursive: true });
300
- execFileSync('tar', [
301
- 'xzf',
302
- join(work, basename(tarball)),
303
- '-C',
304
- dir,
305
- '--strip-components=1',
306
- ]);
307
- if (spec.id === 'typescript') rebuildTypescript(dir, work);
956
+ if (spec.id === 'objcpp') {
957
+ dir = vendorObjcpp(work, spec.pkg);
958
+ } else {
959
+ const out = execFileSync(
960
+ 'npm',
961
+ ['pack', spec.pkg, '--silent', '--pack-destination', work],
962
+ {
963
+ cwd: work,
964
+ encoding: 'utf8',
965
+ }
966
+ );
967
+ const tarball = out.trim().split('\n').pop() ?? '';
968
+ dir = join(work, spec.pkg.replace(/[^a-z0-9-]/gi, '_'));
969
+ mkdirSync(dir, { recursive: true });
970
+ execFileSync('tar', [
971
+ 'xzf',
972
+ join(work, basename(tarball)),
973
+ '-C',
974
+ dir,
975
+ '--strip-components=1',
976
+ ]);
977
+ if (spec.id === 'typescript' || spec.id === 'tsx') rebuildTypescript(dir, work, only);
978
+ if (spec.id === 'javascript') rebuildJavascript(dir, work);
979
+ if (spec.id === 'swift') rebuildSwift(dir, work);
980
+ if (spec.id === 'sql') rebuildSql(dir, work);
981
+ if (spec.id === 'groovy') rebuildGroovy(dir, work);
982
+ if (spec.id === 'cpp') rebuildCpp(dir, work);
983
+ if (spec.id === 'bash') rebuildBash(dir, work);
984
+ if (spec.id === 'nix' || spec.id === 'cmake') rebuildNixOrCmake(dir, work, spec.id);
985
+ }
308
986
  extracted.set(spec.pkg, dir);
309
987
  } catch (err) {
310
988
  problems.push({
@@ -322,8 +1000,24 @@ function vendor(): Problem[] {
322
1000
  problems.push({ id: spec.id, detail: `${spec.pkg} ships no ${wasmName}` });
323
1001
  } else {
324
1002
  copyFileSync(wasmSrc, join(WASM_DIR, `${spec.id}.wasm`));
1003
+ if (spec.id === 'lua') copyFileSync(join(dir, 'LICENSE.md'), join(WASM_DIR, 'lua.LICENSE'));
1004
+ if (spec.id === 'terraform')
1005
+ copyFileSync(join(dir, 'LICENSE'), join(WASM_DIR, 'terraform.LICENSE'));
1006
+ if (
1007
+ spec.id === 'javascript' ||
1008
+ spec.id === 'tsx' ||
1009
+ spec.id === 'objcpp' ||
1010
+ spec.id === 'groovy' ||
1011
+ spec.id === 'cpp' ||
1012
+ spec.id === 'bash' ||
1013
+ spec.id === 'nix' ||
1014
+ spec.id === 'cmake'
1015
+ )
1016
+ chmodSync(join(WASM_DIR, `${spec.id}.wasm`), 0o644);
325
1017
  }
326
1018
 
1019
+ // A targeted JS scanner rebuild must retain our module-scope binding query additions.
1020
+ if (only === 'javascript') continue;
327
1021
  if (spec.tags === 'upstream') {
328
1022
  const tagsSrc = join(dir, 'queries', 'tags.scm');
329
1023
  if (!existsSync(tagsSrc)) {
@@ -363,13 +1057,13 @@ if (problems.length > 0) {
363
1057
  }
364
1058
 
365
1059
  const sizes = GRAMMARS.map((g) => statSync(join(WASM_DIR, `${g.id}.wasm`)).size);
366
- const totalMB = +(sizes.reduce((a, b) => a + b, 0) / 1048576).toFixed(1);
1060
+ const totalBytes = sizes.reduce((a, b) => a + b, 0);
1061
+ const totalMB = +(totalBytes / 1048576).toFixed(1);
367
1062
  console.log(
368
1063
  `VENDOR_GRAMMARS_OK ${JSON.stringify({ grammars: GRAMMARS.length, totalWasmMB: totalMB })}`
369
1064
  );
370
- // The plan's ceiling. Install size is a cost every user pays, so it fails the
371
- // build rather than drifting upward one grammar at a time.
372
- if (totalMB > 40) {
373
- console.error(`VENDOR_GRAMMARS_TOO_LARGE ${totalMB}MB exceeds the 40MB ceiling`);
1065
+ // Owner-approved Objective-C++ cost: repaired bundle 43.58 MiB (43.67 with Terraform 667475e93), bounded at 45 MiB.
1066
+ if (totalBytes > 45 * 1048576) {
1067
+ console.error(`VENDOR_GRAMMARS_TOO_LARGE ${totalMB}MB exceeds the 45MB ceiling`);
374
1068
  process.exit(1);
375
1069
  }