lsr-text-catalog 0.1.2

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/dist/compiled/index.d.ts +12 -0
  4. package/dist/compiled/index.d.ts.map +1 -0
  5. package/dist/compiled/index.js +39 -0
  6. package/dist/compiled/index.js.map +1 -0
  7. package/dist/language.d.ts +4 -0
  8. package/dist/language.d.ts.map +1 -0
  9. package/dist/language.js +50 -0
  10. package/dist/language.js.map +1 -0
  11. package/dist/runtime/index.d.ts +17 -0
  12. package/dist/runtime/index.d.ts.map +1 -0
  13. package/dist/runtime/index.js +68 -0
  14. package/dist/runtime/index.js.map +1 -0
  15. package/dist/types.d.ts +37 -0
  16. package/dist/types.d.ts.map +1 -0
  17. package/dist/types.js +2 -0
  18. package/dist/types.js.map +1 -0
  19. package/dist/vite/index.d.ts +21 -0
  20. package/dist/vite/index.d.ts.map +1 -0
  21. package/dist/vite/index.js +559 -0
  22. package/dist/vite/index.js.map +1 -0
  23. package/dist/vite/manifest.d.ts +24 -0
  24. package/dist/vite/manifest.d.ts.map +1 -0
  25. package/dist/vite/manifest.js +110 -0
  26. package/dist/vite/manifest.js.map +1 -0
  27. package/dist/vite/transform.d.ts +5 -0
  28. package/dist/vite/transform.d.ts.map +1 -0
  29. package/dist/vite/transform.js +873 -0
  30. package/dist/vite/transform.js.map +1 -0
  31. package/dist/vite/types.d.ts +23 -0
  32. package/dist/vite/types.d.ts.map +1 -0
  33. package/dist/vite/types.js +2 -0
  34. package/dist/vite/types.js.map +1 -0
  35. package/package.json +99 -0
  36. package/src/compiled/index.ts +81 -0
  37. package/src/language.ts +62 -0
  38. package/src/runtime/index.ts +126 -0
  39. package/src/types.ts +55 -0
  40. package/src/vite/index.ts +642 -0
  41. package/src/vite/manifest.ts +158 -0
  42. package/src/vite/transform.ts +1228 -0
  43. package/src/vite/types.ts +27 -0
@@ -0,0 +1,873 @@
1
+ import { parse as parseJavaScript } from "@babel/parser";
2
+ import traverseModule, {} from "@babel/traverse";
3
+ import * as t from "@babel/types";
4
+ import { createTransformContext, isFnExpression, isMemberExpression, parse as parseHtml, } from "@vue/compiler-dom";
5
+ import MagicString from "magic-string";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { dirname, extname, isAbsolute, resolve } from "node:path";
8
+ import * as ts from "typescript";
9
+ import { compileScript, parse as parseSfc, registerTS, } from "@vue/compiler-sfc";
10
+ const babelTraverse = traverseModule;
11
+ const traverse = typeof traverseModule === "function"
12
+ ? traverseModule
13
+ : babelTraverse.default;
14
+ registerTS(() => ts);
15
+ const globals = {
16
+ $langText: "langText",
17
+ $langTextPlural: "langTextPlural",
18
+ $langHtmlText: "langHtmlText",
19
+ };
20
+ const runtimeHelpers = {
21
+ langText: "pgettext",
22
+ langTextPlural: "npgettext",
23
+ langHtmlText: "htmlPgettext",
24
+ };
25
+ const scriptExtensions = {
26
+ ".js": true,
27
+ ".mjs": true,
28
+ ".cjs": true,
29
+ ".ts": true,
30
+ ".mts": true,
31
+ ".cts": true,
32
+ ".jsx": true,
33
+ ".tsx": true,
34
+ };
35
+ const scriptLanguages = {
36
+ js: true,
37
+ javascript: true,
38
+ ts: true,
39
+ typescript: true,
40
+ jsx: true,
41
+ tsx: true,
42
+ };
43
+ function macroExport(module, name) {
44
+ if (module === "catalog")
45
+ return name === "text" ? "text" : undefined;
46
+ if (name === "text" ||
47
+ name === "langText" ||
48
+ name === "langTextPlural" ||
49
+ name === "langHtmlText")
50
+ return name;
51
+ return undefined;
52
+ }
53
+ function importedName(specifier) {
54
+ const name = t.isImportSpecifier(specifier)
55
+ ? specifier.imported
56
+ : specifier.local;
57
+ return t.isIdentifier(name) ? name.name : name.value;
58
+ }
59
+ function literal(value) {
60
+ // Also safe in script raw text, interpolation delimiters, and dynamic directive names.
61
+ return JSON.stringify(value).replace(/[<>&'{}\[\]=/\u2028\u2029\s]/gu, (char) => {
62
+ if (char === " ")
63
+ return "\\u0020";
64
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
65
+ });
66
+ }
67
+ function encodeAttribute(value) {
68
+ return value.replace(/[&"'<>\s=`]/gu, (char) => `&#${char.charCodeAt(0)};`);
69
+ }
70
+ function unwrapKey(node) {
71
+ while (t.isTSAsExpression(node) ||
72
+ t.isTSSatisfiesExpression(node) ||
73
+ t.isTSNonNullExpression(node) ||
74
+ t.isTypeCastExpression(node)) {
75
+ node = node.expression;
76
+ }
77
+ return node;
78
+ }
79
+ function isTypeReference(path) {
80
+ for (let current = path.parentPath; current; current = current.parentPath) {
81
+ const node = current.node;
82
+ if (t.isTSAsExpression(node) ||
83
+ t.isTSSatisfiesExpression(node) ||
84
+ t.isTSNonNullExpression(node)) {
85
+ if (node.expression === path.node)
86
+ return false;
87
+ continue;
88
+ }
89
+ if (current.isExportSpecifier() && current.node.exportKind === "type")
90
+ return true;
91
+ if (current.isExportNamedDeclaration() &&
92
+ current.node.exportKind === "type")
93
+ return true;
94
+ if (current.isTSType() ||
95
+ current.isTSTypeAnnotation() ||
96
+ current.isTypeAnnotation())
97
+ return true;
98
+ if (current.isExpression() || current.isStatement())
99
+ return false;
100
+ }
101
+ return false;
102
+ }
103
+ /** Expand only original application sources, before Vue owns SFC compilation. */
104
+ export async function transformTextCatalog(source, id, snapshot, options) {
105
+ if (id.includes("?") ||
106
+ id.includes("#") ||
107
+ id.includes("\0") ||
108
+ id.startsWith("virtual:") ||
109
+ /[/\\]node_modules[/\\]/u.test(id))
110
+ return null;
111
+ const extension = extname(id);
112
+ if (extension !== ".vue" && !Object.hasOwn(scriptExtensions, extension))
113
+ return null;
114
+ const filename = isAbsolute(id) ? id : resolve(options.root, id);
115
+ const output = new MagicString(source);
116
+ let changed = false;
117
+ let serial = 0;
118
+ let setupScript;
119
+ let normalScript;
120
+ const scripts = [];
121
+ const reservedNames = new Set();
122
+ function fail(message, offset) {
123
+ const before = source.slice(0, offset);
124
+ const line = before.split("\n").length;
125
+ const column = offset - before.lastIndexOf("\n");
126
+ const error = new Error(`[textCatalog] ${id}:${line}:${column}: ${message}`);
127
+ Object.assign(error, {
128
+ id,
129
+ pos: offset,
130
+ loc: { file: id, line, column: column - 1 },
131
+ });
132
+ throw error;
133
+ }
134
+ const moduleIdentities = new Map();
135
+ function moduleIdentity(specifier) {
136
+ return moduleIdentities.get(specifier);
137
+ }
138
+ async function hasCatalogImport(content, importer) {
139
+ // Unsupported preprocessors cannot be parsed as JS; resolve their import
140
+ // specifiers before deciding that macro usage may safely pass through.
141
+ for (const match of content.matchAll(/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)['"]([^'"]+)['"]/gu)) {
142
+ if (await options.resolveModule(match[1], importer))
143
+ return true;
144
+ }
145
+ return false;
146
+ }
147
+ function parseCode(content, language, offset) {
148
+ const plugins = [];
149
+ if (["ts", "typescript", "tsx", "mts", "cts"].includes(language))
150
+ plugins.push("typescript");
151
+ if (["jsx", "tsx"].includes(language))
152
+ plugins.push("jsx");
153
+ try {
154
+ return parseJavaScript(content, {
155
+ sourceType: "unambiguous",
156
+ plugins,
157
+ allowAwaitOutsideFunction: true,
158
+ });
159
+ }
160
+ catch (error) {
161
+ const parseError = error;
162
+ return fail(`Cannot parse ${language} source: ${parseError.message}`, offset + (parseError.pos ?? 0));
163
+ }
164
+ }
165
+ async function readScript(content, language, offset) {
166
+ const ast = parseCode(content, language, offset);
167
+ const specifiers = new Set();
168
+ t.traverseFast(ast, (node) => {
169
+ if ((t.isImportDeclaration(node) ||
170
+ t.isExportNamedDeclaration(node) ||
171
+ t.isExportAllDeclaration(node)) &&
172
+ node.source) {
173
+ specifiers.add(node.source.value);
174
+ }
175
+ else if (t.isImportExpression(node) && t.isStringLiteral(node.source)) {
176
+ specifiers.add(node.source.value);
177
+ }
178
+ else if (t.isCallExpression(node) &&
179
+ (t.isImport(node.callee) ||
180
+ t.isIdentifier(node.callee, { name: "require" })) &&
181
+ t.isStringLiteral(node.arguments[0])) {
182
+ specifiers.add(node.arguments[0].value);
183
+ }
184
+ });
185
+ await Promise.all([...specifiers]
186
+ .filter((specifier) => !moduleIdentities.has(specifier))
187
+ .map(async (specifier) => {
188
+ moduleIdentities.set(specifier, await options.resolveModule(specifier, filename));
189
+ }));
190
+ const script = {
191
+ ast,
192
+ content,
193
+ offset,
194
+ imports: new Map(),
195
+ bindings: new Map(),
196
+ helpers: new Map(),
197
+ };
198
+ traverse(ast, {
199
+ Identifier(path) {
200
+ reservedNames.add(path.node.name);
201
+ },
202
+ Program(path) {
203
+ script.bindings = new Map(Object.entries(path.scope.bindings));
204
+ for (const statement of path.node.body) {
205
+ if (t.isExportAllDeclaration(statement) &&
206
+ statement.exportKind !== "type" &&
207
+ moduleIdentity(statement.source.value)) {
208
+ fail("Re-exporting a compiler-macro module is unsupported. Import macros directly at their call sites; re-export runtime or type exports explicitly.", offset + statement.start);
209
+ }
210
+ if (t.isExportNamedDeclaration(statement) &&
211
+ statement.source &&
212
+ statement.exportKind !== "type") {
213
+ const module = moduleIdentity(statement.source.value);
214
+ if (module &&
215
+ statement.specifiers.some((specifier) => !t.isExportSpecifier(specifier) ||
216
+ (specifier.exportKind !== "type" &&
217
+ macroExport(module, importedName(specifier))))) {
218
+ fail("Compiler macros cannot be re-exported. Import them directly from the canonical module at each call site.", offset + statement.start);
219
+ }
220
+ }
221
+ if (!t.isImportDeclaration(statement) ||
222
+ statement.importKind === "type")
223
+ continue;
224
+ const module = moduleIdentity(statement.source.value);
225
+ if (!module)
226
+ continue;
227
+ for (const specifier of statement.specifiers) {
228
+ if (t.isImportDefaultSpecifier(specifier))
229
+ continue;
230
+ if (t.isImportSpecifier(specifier) &&
231
+ specifier.importKind === "type")
232
+ continue;
233
+ const macro = t.isImportSpecifier(specifier)
234
+ ? macroExport(module, importedName(specifier))
235
+ : undefined;
236
+ if (!macro && !t.isImportNamespaceSpecifier(specifier))
237
+ continue;
238
+ const binding = path.scope.getBinding(specifier.local.name);
239
+ if (binding.constantViolations.length)
240
+ fail("Compiler macro imports cannot be assigned to.", offset + binding.constantViolations[0].node.start);
241
+ script.imports.set(binding, {
242
+ binding,
243
+ module,
244
+ macro,
245
+ declaration: statement,
246
+ specifier,
247
+ runtimeUse: false,
248
+ typeUse: false,
249
+ references: new WeakSet(),
250
+ });
251
+ }
252
+ }
253
+ },
254
+ });
255
+ scripts.push(script);
256
+ return script;
257
+ }
258
+ function helper(script, name) {
259
+ const existing = script.helpers.get(name);
260
+ if (existing)
261
+ return existing;
262
+ let local;
263
+ do
264
+ local = `__textCatalog_${name}_${serial++}`;
265
+ while (reservedNames.has(local) || source.includes(local));
266
+ script.helpers.set(name, local);
267
+ return local;
268
+ }
269
+ function replace(region, start, end, value) {
270
+ output.overwrite(region.position(start), region.position(end), region.encode(value));
271
+ changed = true;
272
+ }
273
+ function transformAst(ast, region, environment, prefix = 0) {
274
+ function location(node) {
275
+ return region.position(Math.max(0, node.start - prefix));
276
+ }
277
+ function resolveImport(path) {
278
+ const binding = path.scope.getBinding(path.node.name);
279
+ if (binding)
280
+ return environment.script?.imports.get(binding);
281
+ if (environment.shadows?.has(path.node.name))
282
+ return undefined;
283
+ if (environment.template)
284
+ return environment.imports?.get(path.node.name);
285
+ const outer = normalScript?.bindings.get(path.node.name);
286
+ return outer ? normalScript?.imports.get(outer) : undefined;
287
+ }
288
+ traverse(ast, {
289
+ "AssignmentExpression|UpdateExpression"(path) {
290
+ if (!environment.template)
291
+ return;
292
+ const target = t.isAssignmentExpression(path.node)
293
+ ? path.node.left
294
+ : t.isUpdateExpression(path.node)
295
+ ? path.node.argument
296
+ : undefined;
297
+ if (!target)
298
+ return;
299
+ for (const name of Object.keys(t.getBindingIdentifiers(target))) {
300
+ if (path.scope.getBinding(name) || environment.shadows?.has(name))
301
+ continue;
302
+ if (environment.imports?.has(name) || Object.hasOwn(globals, name)) {
303
+ fail("Compiler macros cannot be assigned to from templates. Use a separate local state binding.", location(target));
304
+ }
305
+ }
306
+ },
307
+ ImportExpression(path) {
308
+ if (t.isStringLiteral(path.node.source) &&
309
+ moduleIdentity(path.node.source.value)) {
310
+ fail("Dynamic import of compiler-macro modules is unsupported. Use named ESM imports.", location(path.node));
311
+ }
312
+ },
313
+ CallExpression(path) {
314
+ const callee = path.node.callee;
315
+ if ((t.isImport(callee) ||
316
+ (t.isIdentifier(callee, { name: "require" }) &&
317
+ !path.scope.getBinding("require"))) &&
318
+ t.isStringLiteral(path.node.arguments[0]) &&
319
+ moduleIdentity(path.node.arguments[0].value)) {
320
+ fail("Dynamic import/require of compiler-macro modules is unsupported. Use named ESM imports, or explicit namespace member calls.", location(path.node));
321
+ }
322
+ },
323
+ ReferencedIdentifier(path) {
324
+ if (!path.isIdentifier() && !path.isJSXIdentifier())
325
+ return;
326
+ const record = resolveImport(path);
327
+ const global = !record &&
328
+ environment.template &&
329
+ !path.scope.hasBinding(path.node.name) &&
330
+ !environment.shadows?.has(path.node.name) &&
331
+ Object.hasOwn(globals, path.node.name)
332
+ ? globals[path.node.name]
333
+ : undefined;
334
+ if (!record && !global)
335
+ return;
336
+ record?.references.add(path.node);
337
+ if (isTypeReference(path)) {
338
+ if (record)
339
+ record.typeUse = true;
340
+ return;
341
+ }
342
+ let macro = record?.macro ?? global;
343
+ let callee = path;
344
+ if (record && !record.macro) {
345
+ const parent = path.parentPath;
346
+ if ((parent.isMemberExpression() ||
347
+ parent.isOptionalMemberExpression()) &&
348
+ parent.node.object === path.node) {
349
+ if (parent.node.computed || !t.isIdentifier(parent.node.property)) {
350
+ fail("Computed namespace access may escape a compiler macro. Use an explicit namespace.langText(...) or namespace.text(...) call.", location(parent.node));
351
+ }
352
+ macro = macroExport(record.module, parent.node.property.name);
353
+ if (!macro) {
354
+ record.runtimeUse = true;
355
+ return;
356
+ }
357
+ if (parent.isOptionalMemberExpression())
358
+ fail("Optional compiler-macro access is unsupported. Use a direct call with a literal key.", location(parent.node));
359
+ callee = parent;
360
+ }
361
+ else {
362
+ fail("A compiler-macro namespace cannot escape as a runtime value. Use explicit namespace member calls or named runtime imports.", location(path.node));
363
+ }
364
+ }
365
+ if (!macro)
366
+ return;
367
+ const call = callee.parentPath;
368
+ if (!call?.isCallExpression() || call.node.callee !== callee.node) {
369
+ fail(`Compiler macro ${macro} must be called directly, not escaped, re-exported, constructed, tagged, or called optionally. Use a literal-key call at the use site.`, location(callee.node));
370
+ }
371
+ const args = call.node.arguments;
372
+ const minimum = macro === "langTextPlural" ? 3 : 1;
373
+ const maximum = macro === "text" ? 1 : macro === "langTextPlural" ? 4 : 2;
374
+ if (call.node.typeParameters ||
375
+ call.node.typeArguments ||
376
+ args.length < minimum ||
377
+ args.length > maximum ||
378
+ args.some((arg) => t.isSpreadElement(arg) || t.isArgumentPlaceholder(arg))) {
379
+ fail(`${macro} requires ${minimum === maximum ? minimum : `${minimum}–${maximum}`} ordinary arguments; spreads, extra arguments and generic calls are unsupported.`, location(call.node));
380
+ }
381
+ const keyNode = unwrapKey(args[0]);
382
+ const key = t.isStringLiteral(keyNode)
383
+ ? keyNode.value
384
+ : t.isTemplateLiteral(keyNode) && keyNode.expressions.length === 0
385
+ ? keyNode.quasis[0]?.value.cooked
386
+ : undefined;
387
+ if (typeof key !== "string")
388
+ fail(`${macro} requires a literal catalog key in compiled mode. Replace finite selectors with literal-key branches, or select runtime mode for dynamic keys.`, location(args[0]));
389
+ const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
390
+ let values;
391
+ if (macro === "langTextPlural") {
392
+ const pluralNode = unwrapKey(args[1]);
393
+ const pluralKey = t.isStringLiteral(pluralNode)
394
+ ? pluralNode.value
395
+ : t.isTemplateLiteral(pluralNode) &&
396
+ pluralNode.expressions.length === 0
397
+ ? pluralNode.quasis[0]?.value.cooked
398
+ : undefined;
399
+ if (typeof pluralKey !== "string")
400
+ fail("langTextPlural requires two literal catalog keys. Replace dynamic selectors with literal-key branches.", location(args[1]));
401
+ const entry = Object.entries(snapshot.plurals).find(([, pair]) => pair.one === key && pair.plural === pluralKey);
402
+ if (!entry)
403
+ fail(`Unknown plural catalog pair ${JSON.stringify(key)}, ${JSON.stringify(pluralKey)}. Both keys must match one declared one/plural pair.`, location(args[0]));
404
+ const [context, pair] = entry;
405
+ if (!own(snapshot.texts, pair.one) ||
406
+ !own(snapshot.texts, pair.plural))
407
+ fail(`Plural catalog key ${JSON.stringify(context)} references missing source keys. Recompile the catalog.`, location(args[0]));
408
+ values = [
409
+ context,
410
+ snapshot.texts[pair.one],
411
+ snapshot.texts[pair.plural],
412
+ ];
413
+ }
414
+ else {
415
+ if (!own(snapshot.texts, key))
416
+ fail(`Unknown catalog key ${JSON.stringify(key)}. Add it to the source catalog or correct the literal.`, location(args[0]));
417
+ if (macro === "langHtmlText" && !own(snapshot.htmlKeys, key))
418
+ fail(`Catalog key ${JSON.stringify(key)} is not declared as HTML. Use langText or declare HTML source copy.`, location(args[0]));
419
+ values = [key, snapshot.texts[key]];
420
+ }
421
+ if (macro === "text") {
422
+ replace(region, call.node.start - prefix, call.node.end - prefix, literal(values[1]));
423
+ return;
424
+ }
425
+ const runtime = runtimeHelpers[macro];
426
+ let target;
427
+ if (global) {
428
+ target = `$${runtime}`;
429
+ if (environment.shadows?.has(target) || path.scope.hasBinding(target))
430
+ fail(`The runtime translation global ${target} is shadowed here. Rename that local binding before using ${macro}.`, location(callee.node));
431
+ }
432
+ else {
433
+ const owner = environment.template ? setupScript : environment.script;
434
+ if (!owner)
435
+ fail("Imported template compiler macros require an inline <script setup> block.", location(callee.node));
436
+ target = helper(owner, runtime);
437
+ }
438
+ // The original remaining arguments stay in place: evaluation happens once,
439
+ // left-to-right, before the adapter selects the current app at invocation.
440
+ replace(region, callee.node.start - prefix, callee.node.end - prefix, target);
441
+ replace(region, args[0].start - prefix, args[macro === "langTextPlural" ? 1 : 0].end - prefix, values.map(literal).join(","));
442
+ },
443
+ });
444
+ }
445
+ let descriptor;
446
+ let templateImports = new Map();
447
+ const templateShadows = new Set();
448
+ if (extension === ".vue") {
449
+ const parsed = parseSfc(source, {
450
+ filename,
451
+ templateParseOptions: { expressionPlugins: ["typescript"] },
452
+ });
453
+ descriptor = parsed.descriptor;
454
+ if (parsed.errors.length) {
455
+ const error = parsed.errors[0];
456
+ fail(`Cannot parse Vue SFC: ${error.message}`, "loc" in error ? (error.loc?.start.offset ?? 0) : 0);
457
+ }
458
+ for (const [block, setup] of [
459
+ [descriptor.script, false],
460
+ [descriptor.scriptSetup, true],
461
+ ]) {
462
+ if (!block ||
463
+ block.src ||
464
+ !Object.hasOwn(scriptLanguages, block.lang ?? "js"))
465
+ continue;
466
+ const script = await readScript(block.content, block.lang ?? "js", block.loc.start.offset);
467
+ if (setup)
468
+ setupScript = script;
469
+ else
470
+ normalScript = script;
471
+ }
472
+ const hasMacroImports = scripts.some((script) => script.imports.size > 0);
473
+ for (const block of descriptor.customBlocks) {
474
+ if (hasMacroImports ||
475
+ (await hasCatalogImport(block.content, filename)) ||
476
+ /(?:\$?lang(?:Html)?Text(?:Plural)?|\btext)\s*\(/u.test(block.content)) {
477
+ fail("Text catalog compilation does not support macro usage in custom SFC blocks.", block.loc.start.offset);
478
+ }
479
+ }
480
+ for (const block of [
481
+ descriptor.script,
482
+ descriptor.scriptSetup,
483
+ descriptor.template,
484
+ ]) {
485
+ if (!block)
486
+ continue;
487
+ const unsupported = block.src ||
488
+ (block.type === "template"
489
+ ? block.lang && block.lang !== "html"
490
+ : !Object.hasOwn(scriptLanguages, block.lang ?? "js"));
491
+ if (!unsupported)
492
+ continue;
493
+ let content = block.content;
494
+ let importer = filename;
495
+ if (block.src) {
496
+ const external = resolve(dirname(filename), block.src);
497
+ importer = external;
498
+ if (existsSync(external))
499
+ content += readFileSync(external, "utf8");
500
+ }
501
+ if (hasMacroImports ||
502
+ (await hasCatalogImport(content, importer)) ||
503
+ /(?:\$?lang(?:Html)?Text(?:Plural)?|\btext)\s*\(/u.test(content + source)) {
504
+ fail(`Text catalog compilation does not support external or preprocessed <${block.type}> blocks. Move macro usage into ordinary inline JS/TS and HTML SFC blocks.`, block.loc.start.offset);
505
+ }
506
+ }
507
+ if ((descriptor.script || descriptor.scriptSetup) &&
508
+ ![descriptor.script, descriptor.scriptSetup].some((block) => block &&
509
+ (block.src || !Object.hasOwn(scriptLanguages, block.lang ?? "js")))) {
510
+ // Vue owns props/Options API metadata and visibility between the two scripts.
511
+ // Use its public analysis rather than treating every spelling as a global.
512
+ const metadata = compileScript(descriptor, {
513
+ id: filename,
514
+ sourceMap: false,
515
+ });
516
+ for (const name of Object.keys(metadata.bindings ?? {}))
517
+ templateShadows.add(name);
518
+ templateImports = new Map();
519
+ if (setupScript) {
520
+ for (const script of scripts) {
521
+ for (const record of script.imports.values()) {
522
+ if (metadata.imports?.[record.specifier.local.name]?.isType === false) {
523
+ templateImports.set(record.specifier.local.name, record);
524
+ templateShadows.delete(record.specifier.local.name);
525
+ }
526
+ }
527
+ }
528
+ }
529
+ }
530
+ }
531
+ else {
532
+ normalScript = await readScript(source, extension.slice(1), 0);
533
+ }
534
+ function reserveTemplateNames(node) {
535
+ const expressions = [];
536
+ if (node.type === 5)
537
+ expressions.push(node.content);
538
+ if (node.type === 1) {
539
+ for (const prop of node.props) {
540
+ if (prop.type !== 7)
541
+ continue;
542
+ if (prop.exp)
543
+ expressions.push(prop.exp);
544
+ if (prop.arg)
545
+ expressions.push(prop.arg);
546
+ if (prop.forParseResult) {
547
+ for (const expression of [
548
+ prop.forParseResult.source,
549
+ prop.forParseResult.value,
550
+ prop.forParseResult.key,
551
+ prop.forParseResult.index,
552
+ ]) {
553
+ if (expression)
554
+ expressions.push(expression);
555
+ }
556
+ }
557
+ }
558
+ for (const child of node.children)
559
+ reserveTemplateNames(child);
560
+ }
561
+ for (const expression of expressions) {
562
+ if (expression.ast) {
563
+ t.traverseFast(expression.ast, (identifier) => {
564
+ if (t.isIdentifier(identifier))
565
+ reservedNames.add(identifier.name);
566
+ });
567
+ }
568
+ else if (expression.type === 4) {
569
+ reservedNames.add(expression.content);
570
+ }
571
+ }
572
+ }
573
+ for (const child of descriptor?.template?.ast?.children ?? [])
574
+ reserveTemplateNames(child);
575
+ for (const script of scripts) {
576
+ transformAst(script.ast, {
577
+ content: script.content,
578
+ position: (offset) => script.offset + offset,
579
+ encode: (value) => value,
580
+ }, { script });
581
+ }
582
+ // Vue decodes entities in both attributes and interpolations, using different
583
+ // HTML rules. Keep the raw suffix of partially consumed named entities mapped
584
+ // to its own UTF-16 offsets rather than to the entity's starting ampersand.
585
+ const entityCaches = {
586
+ attribute: new Map(),
587
+ text: new Map(),
588
+ };
589
+ function decodeEntityToken(token, attribute) {
590
+ const cache = attribute ? entityCaches.attribute : entityCaches.text;
591
+ const cached = cache.get(token);
592
+ if (cached !== undefined)
593
+ return cached;
594
+ let value;
595
+ if (attribute) {
596
+ const element = parseHtml(`<i value="${token}"/>`).children[0];
597
+ const prop = element.props[0];
598
+ value = prop.type === 6 ? prop.value.content : token;
599
+ }
600
+ else {
601
+ // Sentinels prevent Vue from discarding whitespace-only text nodes.
602
+ const text = parseHtml(`_${token}_`, { whitespace: "preserve" })
603
+ .children[0];
604
+ value = text.type === 2 ? text.content.slice(1, -1) : token;
605
+ }
606
+ cache.set(token, value);
607
+ return value;
608
+ }
609
+ function regionFor(expression, attribute, argument = false) {
610
+ if (expression.type !== 4)
611
+ return fail("Expected an original, uncompiled Vue expression.", expression.loc.start.offset);
612
+ let start = expression.loc.start.offset;
613
+ let raw = expression.loc.source;
614
+ if (argument && raw.startsWith("[") && raw.endsWith("]")) {
615
+ start++;
616
+ raw = raw.slice(1, -1);
617
+ }
618
+ const offsets = [];
619
+ if (!argument && raw !== expression.content) {
620
+ let decoded = "";
621
+ let rawOffset = 0;
622
+ for (const match of raw.matchAll(/&(?:#[xX][\da-fA-F]+;?|#\d+;?|[a-zA-Z][a-zA-Z\d]*;?)=?/gu)) {
623
+ for (; rawOffset < match.index; rawOffset++) {
624
+ offsets.push(start + rawOffset);
625
+ decoded += raw[rawOffset];
626
+ }
627
+ const token = match[0];
628
+ const value = decodeEntityToken(token, attribute);
629
+ let suffix = 0;
630
+ while (suffix < value.length &&
631
+ value[value.length - suffix - 1] === token[token.length - suffix - 1])
632
+ suffix++;
633
+ // A decoded character can itself equal a raw trailing character
634
+ // (e.g. &semi;). Only preserve a suffix the parser left untouched.
635
+ while (suffix &&
636
+ decodeEntityToken(token.slice(0, -suffix), attribute) !==
637
+ value.slice(0, -suffix))
638
+ suffix--;
639
+ for (let index = 0; index < value.length - suffix; index++)
640
+ offsets.push(start + rawOffset);
641
+ for (let index = token.length - suffix; index < token.length; index++)
642
+ offsets.push(start + rawOffset + index);
643
+ decoded += value;
644
+ rawOffset += token.length;
645
+ }
646
+ for (; rawOffset < raw.length; rawOffset++) {
647
+ offsets.push(start + rawOffset);
648
+ decoded += raw[rawOffset];
649
+ }
650
+ offsets.push(start + raw.length);
651
+ if (decoded !== expression.content)
652
+ fail("Cannot map the decoded Vue expression safely. Use semicolon-terminated HTML entities or move this expression into script setup.", start);
653
+ }
654
+ return {
655
+ content: expression.content,
656
+ position: offsets.length
657
+ ? (offset) => offsets[offset]
658
+ : (offset) => start + offset,
659
+ encode: attribute && !argument ? encodeAttribute : (value) => value,
660
+ };
661
+ }
662
+ function expressionAst(region, shadows, grammar = "expression", inlineEvent = false) {
663
+ const prefix = grammar === "statements"
664
+ ? "($event)=>{"
665
+ : inlineEvent
666
+ ? "($event)=>("
667
+ : "(";
668
+ const suffix = grammar === "statements" ? "\n}" : grammar === "params" ? ")=>{}" : "\n)";
669
+ const ast = parseCode(prefix + region.content + suffix, "ts", region.position(0) - prefix.length);
670
+ transformAst(ast, region, { template: true, imports: templateImports, shadows }, prefix.length);
671
+ return ast;
672
+ }
673
+ function parameterNames(content, offset) {
674
+ const ast = parseCode(`(${content})=>{}`, "ts", offset - 1);
675
+ const statement = ast.program.body[0];
676
+ if (!t.isExpressionStatement(statement) ||
677
+ !t.isArrowFunctionExpression(statement.expression))
678
+ fail("Expected Vue parameter bindings.", offset);
679
+ return statement.expression.params.flatMap((parameter) => Object.keys(t.getBindingIdentifiers(parameter)));
680
+ }
681
+ let eventContext;
682
+ function visitTemplate(node, inherited) {
683
+ if (node.type === 5) {
684
+ expressionAst(regionFor(node.content, false), inherited);
685
+ return;
686
+ }
687
+ if (node.type !== 1)
688
+ return;
689
+ const directives = node.props.filter((prop) => prop.type === 7);
690
+ const loop = directives.find((prop) => prop.name === "for");
691
+ const slot = directives.find((prop) => prop.name === "slot");
692
+ const locals = new Set(inherited);
693
+ if (loop?.exp) {
694
+ const result = loop.forParseResult;
695
+ if (!result || loop.exp.type !== 4)
696
+ fail("Cannot parse v-for. Use Vue's (value, key, index) in source syntax.", loop.loc.start.offset);
697
+ const region = regionFor(loop.exp, true);
698
+ const segments = [result.value, result.key, result.index].filter((part) => !!part);
699
+ for (const part of segments) {
700
+ if (part.type !== 4)
701
+ continue;
702
+ for (const name of parameterNames(part.content, part.loc.start.offset))
703
+ locals.add(name);
704
+ }
705
+ const subregion = (part) => {
706
+ if (part.type !== 4)
707
+ fail("Expected an original v-for expression.", part.loc.start.offset);
708
+ const offset = part.loc.start.offset - loop.exp.loc.start.offset;
709
+ return {
710
+ content: part.content,
711
+ position: (index) => region.position(offset + index),
712
+ encode: region.encode,
713
+ };
714
+ };
715
+ expressionAst(subregion(result.source), inherited);
716
+ for (const part of segments)
717
+ expressionAst(subregion(part), locals, "params");
718
+ }
719
+ const slotLocals = new Set(locals);
720
+ if (slot?.exp?.type === 4) {
721
+ for (const name of parameterNames(slot.exp.content, slot.exp.loc.start.offset))
722
+ slotLocals.add(name);
723
+ expressionAst(regionFor(slot.exp, true), slotLocals, "params");
724
+ }
725
+ for (const directive of directives) {
726
+ // v-if has higher precedence than v-for on the same element. Slot
727
+ // parameters belong to slot children, not the component's own props.
728
+ const scope = directive.name === "if" || directive.name === "else-if"
729
+ ? inherited
730
+ : locals;
731
+ if (directive.arg && directive.arg.type === 4 && !directive.arg.isStatic)
732
+ expressionAst(regionFor(directive.arg, false, true), scope);
733
+ if (directive.name === "for" || directive.name === "slot")
734
+ continue;
735
+ if (directive.exp) {
736
+ let inlineEvent = false;
737
+ let grammar = "expression";
738
+ if (directive.name === "on" && directive.arg) {
739
+ eventContext ??= createTransformContext(descriptor.template.ast, {
740
+ expressionPlugins: ["typescript"],
741
+ });
742
+ inlineEvent =
743
+ !isMemberExpression(directive.exp, eventContext) &&
744
+ !isFnExpression(directive.exp, eventContext);
745
+ if (inlineEvent &&
746
+ directive.exp.type === 4 &&
747
+ directive.exp.content.includes(";"))
748
+ grammar = "statements";
749
+ }
750
+ expressionAst(regionFor(directive.exp, true), scope, grammar, inlineEvent);
751
+ }
752
+ else if (directive.name === "bind" &&
753
+ directive.arg?.type === 4 &&
754
+ directive.arg.isStatic) {
755
+ const name = directive.arg.content.replace(/-([a-z])/gu, (_, char) => char.toUpperCase());
756
+ if (!scope.has(name) &&
757
+ (templateImports.has(name) || Object.hasOwn(globals, name)))
758
+ fail("A compiler macro cannot escape through v-bind shorthand. Call it with a literal key.", directive.loc.start.offset);
759
+ }
760
+ }
761
+ // Component tags, directive identifiers and refs can also consume setup
762
+ // imports without containing an expression; none may escape a macro.
763
+ for (const [name, record] of templateImports) {
764
+ if (locals.has(name))
765
+ continue;
766
+ const kebab = name
767
+ .replace(/\B[A-Z]/gu, (char) => `-${char.toLowerCase()}`)
768
+ .toLowerCase();
769
+ if ((node.tagType === 1 && (node.tag === name || node.tag === kebab)) ||
770
+ directives.some((directive) => `v-${directive.name}` === kebab)) {
771
+ fail("Compiler macros cannot be used as components or directives. Call them with literal keys.", node.loc.start.offset);
772
+ }
773
+ if (node.tagType === 1 && node.tag.startsWith(`${name}.`)) {
774
+ const member = node.tag.slice(name.length + 1).split(".")[0];
775
+ if (record.macro || macroExport(record.module, member))
776
+ fail("Compiler macros cannot escape as namespaced components. Call them with literal keys.", node.loc.start.offset);
777
+ record.runtimeUse = true;
778
+ }
779
+ }
780
+ for (const prop of node.props) {
781
+ if (prop.type === 6 &&
782
+ prop.name === "ref" &&
783
+ prop.value &&
784
+ templateImports.has(prop.value.content) &&
785
+ !locals.has(prop.value.content))
786
+ fail("A compiler macro cannot be used as a template ref.", prop.loc.start.offset);
787
+ }
788
+ for (const child of node.children)
789
+ visitTemplate(child, slotLocals);
790
+ }
791
+ if (descriptor?.template &&
792
+ !descriptor.template.src &&
793
+ (!descriptor.template.lang || descriptor.template.lang === "html")) {
794
+ for (const child of descriptor.template.ast?.children ?? [])
795
+ visitTemplate(child, templateShadows);
796
+ }
797
+ for (const script of scripts) {
798
+ const declarations = new Map();
799
+ for (const record of script.imports.values()) {
800
+ for (const reference of record.binding.referencePaths) {
801
+ if (record.references.has(reference.node))
802
+ continue;
803
+ if (isTypeReference(reference)) {
804
+ record.typeUse = true;
805
+ continue;
806
+ }
807
+ fail("Unsupported compiler-macro reference. Import the macro directly and call it with a literal key instead of exposing a runtime value.", script.offset + reference.node.start);
808
+ }
809
+ const records = declarations.get(record.declaration) ?? new Map();
810
+ records.set(record.specifier, record);
811
+ declarations.set(record.declaration, records);
812
+ }
813
+ for (const [declaration, records] of declarations) {
814
+ const kept = declaration.specifiers.flatMap((specifier) => {
815
+ const record = records.get(specifier);
816
+ if (record && !record.runtimeUse && !record.typeUse)
817
+ return [];
818
+ const code = script.content.slice(specifier.start, specifier.end);
819
+ return [
820
+ {
821
+ specifier,
822
+ code: record && !record.runtimeUse && record.typeUse
823
+ ? `type ${code}`
824
+ : code,
825
+ typeOnly: !!record && !record.runtimeUse && record.typeUse,
826
+ },
827
+ ];
828
+ });
829
+ if (kept.length === declaration.specifiers.length &&
830
+ kept.every((part) => !part.typeOnly))
831
+ continue;
832
+ const from = script.content.slice(declaration.source.start, declaration.source.end);
833
+ const tail = script.content.slice(declaration.source.end, declaration.end);
834
+ const ordinary = kept.filter((part) => !t.isImportSpecifier(part.specifier));
835
+ const named = kept.filter((part) => t.isImportSpecifier(part.specifier));
836
+ const imports = [
837
+ ...ordinary.filter((part) => !part.typeOnly).map((part) => part.code),
838
+ ...(named.length
839
+ ? [`{ ${named.map((part) => part.code).join(", ")} }`]
840
+ : []),
841
+ ];
842
+ const typeNamespaces = ordinary
843
+ .filter((part) => part.typeOnly)
844
+ .map((part) => `import type ${script.content.slice(part.specifier.start, part.specifier.end)} from ${from};\n`)
845
+ .join("");
846
+ const replacement = imports.length
847
+ ? `import ${imports.join(", ")} from ${from}${tail}`
848
+ : `import ${from}${tail}`;
849
+ output.overwrite(script.offset + declaration.start, script.offset + declaration.end, typeNamespaces + replacement);
850
+ changed = true;
851
+ }
852
+ if (script.helpers.size) {
853
+ const names = [...script.helpers]
854
+ .map(([name, local]) => `${name} as ${local}`)
855
+ .join(", ");
856
+ // Inserted imports intentionally have no invented original call location.
857
+ const position = script.ast.program.interpreter?.end ?? 0;
858
+ output.appendLeft(script.offset + position, `\nimport { ${names} } from ${JSON.stringify(options.facade)};\n`);
859
+ }
860
+ }
861
+ if (!changed)
862
+ return null;
863
+ const map = output.generateMap({
864
+ source: id,
865
+ includeContent: true,
866
+ hires: true,
867
+ });
868
+ return {
869
+ code: output.toString(),
870
+ map: Object.assign(map, { sourcesContent: map.sourcesContent ?? [source] }),
871
+ };
872
+ }
873
+ //# sourceMappingURL=transform.js.map