@backtickjs/compiler 0.0.0-stage → 0.1.1

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -2
  3. package/dist/CodeInformation.d.ts +8 -0
  4. package/dist/CodeInformation.js +2 -0
  5. package/dist/CodeInformation.js.map +1 -0
  6. package/dist/SourceRange.d.ts +4 -0
  7. package/dist/SourceRange.js +2 -0
  8. package/dist/SourceRange.js.map +1 -0
  9. package/dist/buildMappings.d.ts +10 -0
  10. package/dist/buildMappings.js +21 -0
  11. package/dist/buildMappings.js.map +1 -0
  12. package/dist/compileModule.d.ts +25 -0
  13. package/dist/compileModule.js +78 -0
  14. package/dist/compileModule.js.map +1 -0
  15. package/dist/compileScript.d.ts +24 -0
  16. package/dist/compileScript.js +66 -0
  17. package/dist/compileScript.js.map +1 -0
  18. package/dist/diagnostics.d.ts +8 -0
  19. package/dist/diagnostics.js +2 -0
  20. package/dist/diagnostics.js.map +1 -0
  21. package/dist/emitScript.d.ts +52 -0
  22. package/dist/emitScript.js +201 -0
  23. package/dist/emitScript.js.map +1 -0
  24. package/dist/emitScripts.d.ts +10 -0
  25. package/dist/emitScripts.js +23 -0
  26. package/dist/emitScripts.js.map +1 -0
  27. package/dist/flattenScripts.d.ts +2 -0
  28. package/dist/flattenScripts.js +10 -0
  29. package/dist/flattenScripts.js.map +1 -0
  30. package/dist/hashText.d.ts +1 -0
  31. package/dist/hashText.js +18 -0
  32. package/dist/hashText.js.map +1 -0
  33. package/dist/hostElements.d.ts +3 -0
  34. package/dist/hostElements.js +28 -0
  35. package/dist/hostElements.js.map +1 -0
  36. package/dist/index.d.ts +16 -0
  37. package/dist/index.js +10 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/isComponentTag.d.ts +1 -0
  40. package/dist/isComponentTag.js +15 -0
  41. package/dist/isComponentTag.js.map +1 -0
  42. package/dist/nodeFactory.d.ts +3 -0
  43. package/dist/nodeFactory.js +5 -0
  44. package/dist/nodeFactory.js.map +1 -0
  45. package/dist/parseFile.d.ts +34 -0
  46. package/dist/parseFile.js +200 -0
  47. package/dist/parseFile.js.map +1 -0
  48. package/dist/printVirtualCode.d.ts +9 -0
  49. package/dist/printVirtualCode.js +52 -0
  50. package/dist/printVirtualCode.js.map +1 -0
  51. package/dist/resolveBindings.d.ts +76 -0
  52. package/dist/resolveBindings.js +240 -0
  53. package/dist/resolveBindings.js.map +1 -0
  54. package/dist/rewriteFile.d.ts +12 -0
  55. package/dist/rewriteFile.js +22 -0
  56. package/dist/rewriteFile.js.map +1 -0
  57. package/dist/rewriteScript.d.ts +27 -0
  58. package/dist/rewriteScript.js +268 -0
  59. package/dist/rewriteScript.js.map +1 -0
  60. package/dist/segmentsToString.d.ts +9 -0
  61. package/dist/segmentsToString.js +4 -0
  62. package/dist/segmentsToString.js.map +1 -0
  63. package/dist/tagRoot.d.ts +2 -0
  64. package/dist/tagRoot.js +10 -0
  65. package/dist/tagRoot.js.map +1 -0
  66. package/dist/transform.d.ts +17 -0
  67. package/dist/transform.js +61 -0
  68. package/dist/transform.js.map +1 -0
  69. package/dist/transpile.d.ts +27 -0
  70. package/dist/transpile.js +41 -0
  71. package/dist/transpile.js.map +1 -0
  72. package/dist/unmangle.d.ts +2 -0
  73. package/dist/unmangle.js +9 -0
  74. package/dist/unmangle.js.map +1 -0
  75. package/dist/virtualScript.d.ts +22 -0
  76. package/dist/virtualScript.js +305 -0
  77. package/dist/virtualScript.js.map +1 -0
  78. package/dist/virtualize.d.ts +8 -0
  79. package/dist/virtualize.js +10 -0
  80. package/dist/virtualize.js.map +1 -0
  81. package/package.json +58 -3
  82. package/src/CodeInformation.ts +14 -0
  83. package/src/SourceRange.ts +4 -0
  84. package/src/buildMappings.ts +31 -0
  85. package/src/compileModule.ts +132 -0
  86. package/src/compileScript.ts +97 -0
  87. package/src/diagnostics.ts +9 -0
  88. package/src/emitScript.ts +297 -0
  89. package/src/emitScripts.ts +39 -0
  90. package/src/flattenScripts.ts +13 -0
  91. package/src/hashText.ts +17 -0
  92. package/src/hostElements.ts +34 -0
  93. package/src/index.ts +20 -0
  94. package/src/isComponentTag.ts +15 -0
  95. package/src/nodeFactory.ts +18 -0
  96. package/src/parseFile.ts +331 -0
  97. package/src/printVirtualCode.ts +93 -0
  98. package/src/resolveBindings.ts +417 -0
  99. package/src/rewriteFile.ts +52 -0
  100. package/src/rewriteScript.ts +413 -0
  101. package/src/segmentsToString.ts +16 -0
  102. package/src/tagRoot.ts +14 -0
  103. package/src/transform.ts +96 -0
  104. package/src/transpile.ts +49 -0
  105. package/src/unmangle.ts +10 -0
  106. package/src/virtualScript.ts +355 -0
  107. package/src/virtualize.ts +29 -0
@@ -0,0 +1,31 @@
1
+ import type { CodeInformation } from "./CodeInformation.js";
2
+ import type { Segment } from "./segmentsToString.js";
3
+
4
+ export interface SourceMapping {
5
+ sourceOffsets: number[];
6
+ generatedOffsets: number[];
7
+ lengths: number[];
8
+ generatedLengths: number[];
9
+ data?: CodeInformation;
10
+ }
11
+
12
+ export function buildMappings(segments: Segment[]): SourceMapping[] {
13
+ const mappings: SourceMapping[] = [];
14
+ let generatedOffset = 0;
15
+ for (const segment of segments) {
16
+ if (typeof segment === "string") {
17
+ generatedOffset += segment.length;
18
+ continue;
19
+ }
20
+ const [text, , sourceOffset, sourceLength, data] = segment;
21
+ mappings.push({
22
+ sourceOffsets: [sourceOffset],
23
+ generatedOffsets: [generatedOffset],
24
+ lengths: [sourceLength],
25
+ generatedLengths: [text.length],
26
+ ...(data && { data }),
27
+ });
28
+ generatedOffset += text.length;
29
+ }
30
+ return mappings;
31
+ }
@@ -0,0 +1,132 @@
1
+ import type ts from "typescript";
2
+ import type { Plugin } from "./compileScript.js";
3
+ import { transform } from "./transform.js";
4
+
5
+ // A loader's compile of one file, as a runtime runs it: Node's, Bun's. The
6
+ // loader reads the file and loads what comes back; this does the rest, the
7
+ // same way for every runtime.
8
+
9
+ /** A compile step's module, as Babel's presets are: its default export makes the step. */
10
+ interface PluginModule {
11
+ default: () => Plugin;
12
+ }
13
+
14
+ /**
15
+ * The framework's compile steps a project names in its `package.json`, made
16
+ * with the project's own `require`:
17
+ *
18
+ * ```json
19
+ * "backtick": { "plugins": ["@backtickjs/react/plugin"] }
20
+ * ```
21
+ */
22
+ export function pluginsFrom(
23
+ packageJson: string,
24
+ require: (specifier: string) => unknown,
25
+ ): Plugin[] {
26
+ const { backtick } = JSON.parse(packageJson) as {
27
+ backtick?: { plugins?: readonly string[] };
28
+ };
29
+ return (backtick?.plugins ?? []).map((specifier) =>
30
+ (require(specifier) as PluginModule).default(),
31
+ );
32
+ }
33
+
34
+ export interface CompileModuleOptions {
35
+ /** The framework's compile steps, as `pluginsFrom` makes them. */
36
+ readonly plugins: readonly Plugin[];
37
+ /** Transforms run after Backtick's, as a runtime needs its own. */
38
+ readonly before?: readonly ts.TransformerFactory<ts.SourceFile>[];
39
+ }
40
+
41
+ /**
42
+ * A TypeScript file, its `cs` scripts included, as an ES module with its
43
+ * source map inline, compiled with the `tsconfig.json` nearest it, as `tsc`
44
+ * and editors find one, so an app and its server can each have their own.
45
+ * Throws the compiler's errors: a refused script is emitted with `null` where
46
+ * its code was, so it must not load.
47
+ */
48
+ export function compileModule(
49
+ ts: typeof import("typescript"),
50
+ fileName: string,
51
+ source: string,
52
+ { plugins, before = [] }: CompileModuleOptions,
53
+ ): string {
54
+ const diagnostics: ts.Diagnostic[] = [];
55
+ const { outputText } = ts.transpileModule(source, {
56
+ fileName,
57
+ compilerOptions: compilerOptionsFor(ts, directoryOf(fileName)),
58
+ transformers: {
59
+ before: [
60
+ transform(ts, (diagnostic) => diagnostics.push(diagnostic), {
61
+ plugins,
62
+ }),
63
+ ...before,
64
+ ],
65
+ },
66
+ });
67
+
68
+ const errors = diagnostics.filter(
69
+ (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error,
70
+ );
71
+ if (errors.length > 0) {
72
+ throw new Error(
73
+ ts.formatDiagnostics(errors, {
74
+ getCanonicalFileName: (name) => name,
75
+ getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
76
+ getNewLine: () => "\n",
77
+ }),
78
+ );
79
+ }
80
+ return outputText;
81
+ }
82
+
83
+ // Read once per config, however many files share it.
84
+ const optionsByConfig = new Map<string, ts.CompilerOptions>();
85
+
86
+ function compilerOptionsFor(
87
+ ts: typeof import("typescript"),
88
+ directory: string,
89
+ ): ts.CompilerOptions {
90
+ const configPath =
91
+ ts.findConfigFile(directory, ts.sys.fileExists, "tsconfig.json") ?? "";
92
+ let options = optionsByConfig.get(configPath);
93
+ if (options === undefined) {
94
+ let fromConfig: ts.CompilerOptions = {};
95
+ if (configPath !== "") {
96
+ const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
97
+ // Parsed whole, so `extends` is followed, as `tsc` follows it.
98
+ fromConfig = ts.parseJsonConfigFileContent(
99
+ config,
100
+ ts.sys,
101
+ directoryOf(configPath),
102
+ ).options;
103
+ }
104
+ // Where `tsc` would write its output says nothing about a file compiled
105
+ // where it loads, but a source map written with it names the file relative
106
+ // to that output, and a runtime resolves the name relative to the file.
107
+ const {
108
+ outDir: _outDir,
109
+ outFile: _outFile,
110
+ rootDir: _rootDir,
111
+ sourceRoot: _sourceRoot,
112
+ mapRoot: _mapRoot,
113
+ ...compiling
114
+ } = fromConfig;
115
+ options = {
116
+ ...compiling,
117
+ module: ts.ModuleKind.ESNext,
118
+ target: ts.ScriptTarget.ESNext,
119
+ sourceMap: false,
120
+ inlineSourceMap: true,
121
+ inlineSources: true,
122
+ };
123
+ optionsByConfig.set(configPath, options);
124
+ }
125
+ return options;
126
+ }
127
+
128
+ // A path's directory, by `/` or `\`, without Node's `path`.
129
+ function directoryOf(path: string): string {
130
+ const end = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
131
+ return end === -1 ? "." : path.slice(0, end);
132
+ }
@@ -0,0 +1,97 @@
1
+ import remapping from "@jridgewell/remapping";
2
+ import type ts from "typescript";
3
+ import type { EmittedScript } from "./emitScript.js";
4
+
5
+ /**
6
+ * A framework's compile step: a module in, the module it compiles to out, with
7
+ * its map into the module it was given. Solid's adapter's `solid()` is one.
8
+ * Synchronous, as a TypeScript transformer is.
9
+ */
10
+ export type Plugin = (
11
+ code: string,
12
+ id: string,
13
+ ) => { code: string; map: string };
14
+
15
+ /** A script as a build writes it, a module table's entry. */
16
+ export interface CompiledScript extends EmittedScript {
17
+ /** The modules the code requires, by specifier, as Metro records them. */
18
+ readonly dependencies: readonly string[];
19
+ }
20
+
21
+ /**
22
+ * A script as a build writes it: the module whose default export it is,
23
+ * through each plugin in turn, then a module table's entry,
24
+ * `(module, exports, require) => { … }`, as webpack and Metro write one: its
25
+ * body the module as CommonJS, JSX kept where no plugin compiled it, the
26
+ * modules it requires its dependencies, its map chained onto the script's own
27
+ * and naming the host file `sourceName`.
28
+ */
29
+ export function compileScript(
30
+ ts: typeof import("typescript"),
31
+ script: EmittedScript,
32
+ sourceName: string,
33
+ plugins: readonly Plugin[],
34
+ ): CompiledScript {
35
+ let code = `export default (\n${script.code}\n);`;
36
+ const maps = [down(renamed(script.map, sourceName))];
37
+ for (const plugin of plugins) {
38
+ const compiled = plugin(code, `${sourceName}.jsx`);
39
+ code = compiled.code;
40
+ maps.unshift(compiled.map);
41
+ }
42
+ // The module as CommonJS, by TypeScript: its imports `require` calls, its
43
+ // default export `exports.default`, so it runs as a function's body.
44
+ const dependencies: string[] = [];
45
+ const output = ts.transpileModule(code, {
46
+ fileName: "module.jsx",
47
+ compilerOptions: {
48
+ jsx: ts.JsxEmit.Preserve,
49
+ module: ts.ModuleKind.CommonJS,
50
+ target: ts.ScriptTarget.ESNext,
51
+ sourceMap: true,
52
+ },
53
+ transformers: { before: [specifiers(ts, dependencies)] },
54
+ });
55
+ maps.unshift(output.sourceMapText!);
56
+ const body = output.outputText.replace(/\n\/\/# sourceMappingURL=.*$/, "");
57
+ // Chained as an array, not by source name: Babel names a map's source by
58
+ // the id's basename.
59
+ const map = remapping(maps, () => null, { excludeContent: true }).toString();
60
+ return {
61
+ code: `(module, exports, require) => {\n${body}\n}`,
62
+ map: down(map),
63
+ dependencies,
64
+ };
65
+ }
66
+
67
+ // What a module imports, by specifier, once each, as TypeScript reads it.
68
+ function specifiers(
69
+ ts: typeof import("typescript"),
70
+ into: string[],
71
+ ): ts.TransformerFactory<ts.SourceFile> {
72
+ return () => (sourceFile) => {
73
+ for (const statement of sourceFile.statements) {
74
+ if (
75
+ ts.isImportDeclaration(statement) &&
76
+ ts.isStringLiteral(statement.moduleSpecifier) &&
77
+ !into.includes(statement.moduleSpecifier.text)
78
+ ) {
79
+ into.push(statement.moduleSpecifier.text);
80
+ }
81
+ }
82
+ return sourceFile;
83
+ };
84
+ }
85
+
86
+ // A script's map with its one source, the host file, named `sourceName`.
87
+ function renamed(map: string, sourceName: string): string {
88
+ const parsed = JSON.parse(map) as object;
89
+ return JSON.stringify({ ...parsed, sources: [sourceName] });
90
+ }
91
+
92
+ // A map whose code moved down a line, as code does that a wrapper starts on a
93
+ // line of its own.
94
+ function down(map: string): string {
95
+ const parsed = JSON.parse(map) as { mappings: string };
96
+ return JSON.stringify({ ...parsed, mappings: `;${parsed.mappings}` });
97
+ }
@@ -0,0 +1,9 @@
1
+ import type { DiagnosticCategory } from "typescript";
2
+ import type { SourceRange } from "./SourceRange.js";
3
+
4
+ export interface Diagnostic {
5
+ range: SourceRange;
6
+ message: string;
7
+ category: DiagnosticCategory;
8
+ code: number;
9
+ }
@@ -0,0 +1,297 @@
1
+ import { addMapping, GenMapping, toEncodedMap } from "@jridgewell/gen-mapping";
2
+ import { eachMapping, TraceMap } from "@jridgewell/trace-mapping";
3
+ import type ts from "typescript";
4
+ import type { ClientScript } from "./parseFile.js";
5
+ import type { BindingResolution, ResolvedParam } from "./resolveBindings.js";
6
+ import { tagRoot } from "./tagRoot.js";
7
+
8
+ /** A script as the client runs it, and where its code came from. */
9
+ export interface EmittedScript {
10
+ /**
11
+ * The script as an expression, `($splice0, …) => body`, a parameter per
12
+ * entry of a script's `params` (see `paramName`). Types are gone and
13
+ * everything else, JSX included, is as the script wrote it, for the
14
+ * framework's own compiler to read when a bundle is built. How it is
15
+ * delivered is the bundle's to write.
16
+ */
17
+ readonly code: string;
18
+ /**
19
+ * The code's source map, into the host file. It carries no
20
+ * `sourcesContent`: the host file is the server's, and a map is shipped.
21
+ */
22
+ readonly map: string;
23
+ }
24
+
25
+ /**
26
+ * What a name the script wrote becomes: one of its entry's parameters, called
27
+ * with the bindings a hole hands over where it has `args`, read as it is where
28
+ * not.
29
+ */
30
+ export interface Edit {
31
+ readonly param: string;
32
+ readonly args?: readonly string[];
33
+ }
34
+
35
+ /**
36
+ * A script parameter's name: its kind and its index in a script's `params`,
37
+ * `$splice0`, `$capture1`. A script cannot bind a name starting with
38
+ * `$`, so none meets one of its own.
39
+ */
40
+ export function paramName(param: ResolvedParam, index: number): string {
41
+ return `$${param.kind}${index}`;
42
+ }
43
+
44
+ // A binding key as `resolveBindings` writes it, `<name>$<fileHash>$<n>`, back
45
+ // to the name the script wrote.
46
+ function sourceName(key: string): string {
47
+ return key.replace(/\$[0-9a-z]+\$\d+$/, "");
48
+ }
49
+
50
+ /**
51
+ * The edits a script's text needs, by the host-file offset of what each
52
+ * replaces: every splice it reads, host tags among them, and every binding it
53
+ * captures. What is a reference, and each parameter's number, was decided by
54
+ * `resolveBindings`.
55
+ */
56
+ export function scriptEdits(
57
+ script: ClientScript,
58
+ bindings: BindingResolution,
59
+ params: readonly ResolvedParam[],
60
+ ): Map<number, Edit> {
61
+ const captureParam = new Map(
62
+ params.flatMap((param, index) =>
63
+ param.kind === "capture" ? [[param.key, index] as const] : [],
64
+ ),
65
+ );
66
+ const captures = [...captureParam.keys()];
67
+ const read = (key: string): string => {
68
+ const index = captureParam.get(key);
69
+ return index === undefined ? sourceName(key) : `$capture${index}`;
70
+ };
71
+
72
+ const edits = new Map<number, Edit>();
73
+ const at = (identifier: ts.Identifier) =>
74
+ script.toSourceRange(identifier).start;
75
+ params.forEach((param, index) => {
76
+ if (param.kind === "capture") {
77
+ return;
78
+ }
79
+ const edit: Edit = {
80
+ param: paramName(param, index),
81
+ args: [...param.bindings, ...captures].map(read),
82
+ };
83
+ for (const ref of param.refs) {
84
+ edits.set(at(ref), edit);
85
+ }
86
+ });
87
+ for (const [identifier, key] of bindings) {
88
+ const index = captureParam.get(key);
89
+ if (
90
+ index !== undefined &&
91
+ identifier.getSourceFile() === script.fileWithPlaceholders
92
+ ) {
93
+ edits.set(at(identifier), { param: `$capture${index}` });
94
+ }
95
+ }
96
+ return edits;
97
+ }
98
+
99
+ /**
100
+ * Emits a script's code by compiling its own text with TypeScript, with
101
+ * `edits` applied.
102
+ *
103
+ * TypeScript is handed the script's text alone, as `parseFile` read it: its
104
+ * template's escapes decoded, every `${…}` hole its placeholder. An edit is
105
+ * found where its name stands in the host file, and the source map is moved
106
+ * there, through the script's own offsets. The script alone, not the host file
107
+ * blanked around it: that would make each script cost what the whole file
108
+ * does.
109
+ */
110
+ export function emitScript(
111
+ ts: typeof import("typescript"),
112
+ script: ClientScript,
113
+ params: readonly string[],
114
+ edits: ReadonlyMap<number, Edit>,
115
+ ): EmittedScript {
116
+ const sourceFile = script.sourceFile;
117
+
118
+ const transformer: ts.TransformerFactory<ts.SourceFile> =
119
+ (context) => (file) => {
120
+ const f = context.factory;
121
+ const editAt = (node: ts.Node) =>
122
+ edits.get(script.toSourceOffset(node.getStart(file)));
123
+ const edited = (edit: Edit, from: ts.Node): ts.Expression => {
124
+ const param = f.createIdentifier(edit.param);
125
+ const node =
126
+ edit.args === undefined
127
+ ? param
128
+ : f.createCallExpression(
129
+ param,
130
+ undefined,
131
+ edit.args.map((arg) => f.createIdentifier(arg)),
132
+ );
133
+ return ts.setOriginalNode(ts.setTextRange(node, from), from);
134
+ };
135
+ // A tag's names where the tag reads a splice: they stay as written, the
136
+ // parameter of the arrow below.
137
+ const asWritten = new Set<ts.Node>();
138
+ // A tag that names a splice, `<$Card>`: JSX can't call where a tag goes,
139
+ // so the element is the body of an arrow its tag is a parameter of, called
140
+ // with the splice where the element stands, as any splice is read:
141
+ // `(($Card) => <$Card …/>)($splice0())`. A capture is its value, read as
142
+ // a tag as it is anywhere.
143
+ const hostTag = (
144
+ node: ts.JsxElement | ts.JsxSelfClosingElement,
145
+ ): ts.Node | undefined => {
146
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
147
+ const root = tagRoot(ts, opening.tagName);
148
+ const edit = root && editAt(root);
149
+ if (root === undefined || edit?.args === undefined) {
150
+ return undefined;
151
+ }
152
+ asWritten.add(opening.tagName);
153
+ if (ts.isJsxElement(node)) {
154
+ asWritten.add(node.closingElement.tagName);
155
+ }
156
+ const call = ts.setTextRange(
157
+ f.createCallExpression(
158
+ f.createParenthesizedExpression(
159
+ f.createArrowFunction(
160
+ undefined,
161
+ undefined,
162
+ [f.createParameterDeclaration(undefined, undefined, root.text)],
163
+ undefined,
164
+ f.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
165
+ ts.visitEachChild(node, visit, context),
166
+ ),
167
+ ),
168
+ undefined,
169
+ [edited(edit, root)],
170
+ ),
171
+ node,
172
+ );
173
+ // Among an element's children, an expression is written in braces.
174
+ return ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent)
175
+ ? f.createJsxExpression(undefined, call)
176
+ : call;
177
+ };
178
+ const visit = (node: ts.Node): ts.Node => {
179
+ // Types are TypeScript's to strip, after this.
180
+ if (ts.isTypeNode(node) || asWritten.has(node)) {
181
+ return node;
182
+ }
183
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
184
+ const tagged = hostTag(node);
185
+ if (tagged !== undefined) {
186
+ return tagged;
187
+ }
188
+ }
189
+ if (ts.isIdentifier(node)) {
190
+ const edit = editAt(node);
191
+ return edit === undefined ? node : edited(edit, node);
192
+ }
193
+ // `{ count }` names a key as well as a value.
194
+ if (ts.isShorthandPropertyAssignment(node)) {
195
+ const edit = editAt(node.name);
196
+ if (edit !== undefined) {
197
+ return f.createPropertyAssignment(
198
+ node.name.text,
199
+ edited(edit, node.name),
200
+ );
201
+ }
202
+ }
203
+ return ts.visitEachChild(node, visit, context);
204
+ };
205
+
206
+ const [statement] = file.statements;
207
+ if (statement === undefined) {
208
+ return file;
209
+ }
210
+ // Where the script stands, so the entry maps to it.
211
+ const entry = ts.setTextRange(
212
+ f.createArrowFunction(
213
+ undefined,
214
+ undefined,
215
+ params.map((name) =>
216
+ f.createParameterDeclaration(undefined, undefined, name),
217
+ ),
218
+ undefined,
219
+ f.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
220
+ ts.isExpressionStatement(statement)
221
+ ? ts.visitNode(statement.expression, visit, ts.isExpression)
222
+ : ts.visitNode(statement, visit, ts.isBlock),
223
+ ),
224
+ statement,
225
+ );
226
+ return f.updateSourceFile(file, [
227
+ ts.setOriginalNode(f.createExpressionStatement(entry), statement),
228
+ ]);
229
+ };
230
+
231
+ const output = ts.transpileModule(script.textWithPlaceholders, {
232
+ fileName: sourceFile.fileName,
233
+ compilerOptions: {
234
+ target: ts.ScriptTarget.ESNext,
235
+ module: ts.ModuleKind.ESNext,
236
+ jsx: ts.JsxEmit.Preserve,
237
+ sourceMap: true,
238
+ alwaysStrict: false,
239
+ removeComments: true,
240
+ },
241
+ transformers: { before: [transformer] },
242
+ });
243
+ // The file's one expression statement, without its `;` or the comment
244
+ // naming a map file.
245
+ const code = output.outputText
246
+ .replace(/\n\/\/# sourceMappingURL=.*$/, "")
247
+ .replace(/;\s*$/, "");
248
+ return {
249
+ code,
250
+ map: moved(output.sourceMapText!, script),
251
+ };
252
+ }
253
+
254
+ // A map into the script's text, moved to where each position stands in the
255
+ // host file. Named as the host file was, not relative to an output file there
256
+ // is none of, so maps from files in different directories can be combined.
257
+ function moved(map: string, script: ClientScript): string {
258
+ const sourceFile = script.sourceFile;
259
+ const lineStarts = [0];
260
+ for (const line of script.textWithPlaceholders.matchAll(/\r\n|\n|\r/g)) {
261
+ lineStarts.push(line.index + line[0].length);
262
+ }
263
+ // Lines are 1-based here, columns 0-based.
264
+ const original = (line: number, column: number) => {
265
+ const at = sourceFile.getLineAndCharacterOfPosition(
266
+ script.toSourceOffset(lineStarts[line - 1]! + column),
267
+ );
268
+ return { line: at.line + 1, column: at.character };
269
+ };
270
+ const into = new GenMapping({ file: sourceFile.fileName });
271
+ eachMapping(new TraceMap(map), (mapping) => {
272
+ if (mapping.originalLine === null) {
273
+ addMapping(into, {
274
+ generated: {
275
+ line: mapping.generatedLine,
276
+ column: mapping.generatedColumn,
277
+ },
278
+ });
279
+ return;
280
+ }
281
+ // TypeScript writes no names, so there are none to carry.
282
+ addMapping(into, {
283
+ generated: {
284
+ line: mapping.generatedLine,
285
+ column: mapping.generatedColumn,
286
+ },
287
+ source: sourceFile.fileName,
288
+ original: original(mapping.originalLine, mapping.originalColumn),
289
+ });
290
+ });
291
+ // TypeScript's own map, with only where it points changed.
292
+ return JSON.stringify({
293
+ ...(JSON.parse(map) as object),
294
+ sources: [sourceFile.fileName],
295
+ mappings: toEncodedMap(into).mappings,
296
+ });
297
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ type CompiledScript,
3
+ compileScript,
4
+ type Plugin,
5
+ } from "./compileScript.js";
6
+ import { parseSourceText } from "./parseFile.js";
7
+ import { rewriteFile } from "./rewriteFile.js";
8
+
9
+ /** A script's emitted code, by where the host wrote it. */
10
+ export interface EmittedScriptAt extends CompiledScript {
11
+ // the offset of its `cs` in the host file
12
+ readonly start: number;
13
+ }
14
+
15
+ /**
16
+ * What each of a file's scripts compiles to for the client, in source order:
17
+ * nested scripts too, each as its own entry.
18
+ */
19
+ export function emitScripts(
20
+ ts: typeof import("typescript"),
21
+ fileName: string,
22
+ sourceText: string,
23
+ // Its framework's compile steps (see `TransformOptions.plugins`).
24
+ plugins: readonly Plugin[] = [],
25
+ ): EmittedScriptAt[] {
26
+ const file = rewriteFile(ts, parseSourceText(ts, fileName, sourceText));
27
+ return Array.from(file.scripts)
28
+ .flatMap(([template, { runtime }]) =>
29
+ runtime === null
30
+ ? []
31
+ : [
32
+ {
33
+ start: template.getStart(file.sourceFile),
34
+ ...compileScript(ts, runtime.emitted, fileName, plugins),
35
+ },
36
+ ],
37
+ )
38
+ .sort((a, b) => a.start - b.start);
39
+ }
@@ -0,0 +1,13 @@
1
+ import type { ClientScript } from "./parseFile.js";
2
+
3
+ // Yields every client script in the file, descending through splices
4
+ export function* flattenScripts(
5
+ scripts: ClientScript[],
6
+ ): Generator<ClientScript> {
7
+ for (const script of scripts) {
8
+ yield script;
9
+ for (const splice of Object.values(script.splices)) {
10
+ yield* flattenScripts(splice.scripts);
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,17 @@
1
+ // 64-bit FNV-1a over the file's text: the per-file content hash that makes
2
+ // compiler output globally distinguishable. The compiler runs one file at a
3
+ // time and cannot hand out coordinated identifiers, and a file's path alone
4
+ // can recur across codebases (two packages each compiled against their own
5
+ // root), so both binding keys and script locations carry this hash. At 64
6
+ // bits an accidental collision between different files is out of reach
7
+ // (the birthday bound sits around four billion files), so a collision
8
+ // implies identical file content, where sharing is correct rather than a
9
+ // mixup.
10
+ export function hashText(input: string): string {
11
+ let hash = 0xcbf29ce484222325n;
12
+ for (let i = 0; i < input.length; i++) {
13
+ hash ^= BigInt(input.charCodeAt(i));
14
+ hash = (hash * 0x100000001b3n) & 0xffffffffffffffffn;
15
+ }
16
+ return hash.toString(36);
17
+ }
@@ -0,0 +1,34 @@
1
+ import type ts from "typescript";
2
+ import type { Diagnostic } from "./diagnostics.js";
3
+ import { isComponentTag } from "./isComponentTag.js";
4
+
5
+ // Each element written in host code: an intrinsic element is drawn by the
6
+ // client, so it belongs in a script. Every JSX node TypeScript parsed is the
7
+ // host's, a splice's included, as a script's JSX is text in its template.
8
+ export function hostElementDiagnostics(
9
+ ts: typeof import("typescript"),
10
+ sourceFile: ts.SourceFile,
11
+ ): Diagnostic[] {
12
+ const diagnostics: Diagnostic[] = [];
13
+ const visit = (node: ts.Node): void => {
14
+ if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
15
+ const { tagName } = node;
16
+ const name = tagName.getText(sourceFile);
17
+ // A member read (`ui.button`) is a value, whatever its case.
18
+ const named = ts.isIdentifier(tagName) || ts.isJsxNamespacedName(tagName);
19
+ if (named && !isComponentTag(name)) {
20
+ diagnostics.push({
21
+ range: { start: tagName.getStart(sourceFile), end: tagName.getEnd() },
22
+ message:
23
+ `\`<${name}>\` is drawn by the client, so it belongs in a ` +
24
+ `script: cs\`<${name}>…</${name}>\`.`,
25
+ category: ts.DiagnosticCategory.Error,
26
+ code: 0,
27
+ });
28
+ }
29
+ }
30
+ ts.forEachChild(node, visit);
31
+ };
32
+ visit(sourceFile);
33
+ return diagnostics;
34
+ }