@backtickjs/bundler 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.
- package/LICENSE +21 -0
- package/README.md +75 -2
- package/dist/bundle/bindingKey.d.ts +1 -0
- package/dist/bundle/bindingKey.js +10 -0
- package/dist/bundle/bindingKey.js.map +1 -0
- package/dist/bundle/buildBundle.d.ts +14 -0
- package/dist/bundle/buildBundle.js +270 -0
- package/dist/bundle/buildBundle.js.map +1 -0
- package/dist/bundle/expandJsxElement.d.ts +3 -0
- package/dist/bundle/expandJsxElement.js +13 -0
- package/dist/bundle/expandJsxElement.js.map +1 -0
- package/dist/bundle/params.d.ts +3 -0
- package/dist/bundle/params.js +10 -0
- package/dist/bundle/params.js.map +1 -0
- package/dist/bundler.d.ts +32 -0
- package/dist/bundler.js +34 -0
- package/dist/bundler.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/print/code.d.ts +28 -0
- package/dist/print/code.js +95 -0
- package/dist/print/code.js.map +1 -0
- package/dist/print/printBundle.d.ts +19 -0
- package/dist/print/printBundle.js +106 -0
- package/dist/print/printBundle.js.map +1 -0
- package/package.json +55 -3
- package/src/bundle/bindingKey.ts +10 -0
- package/src/bundle/buildBundle.ts +353 -0
- package/src/bundle/expandJsxElement.ts +26 -0
- package/src/bundle/params.ts +15 -0
- package/src/bundler.ts +66 -0
- package/src/index.ts +7 -0
- package/src/print/code.ts +144 -0
- package/src/print/printBundle.ts +126 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
// What a bundle is written as, and the one place its conventions live: how a
|
|
3
|
+
// name is written, how a literal escapes, and how an element is written as JSX
|
|
4
|
+
// for the framework's compiler. Everything is code, as a string.
|
|
5
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
6
|
+
export function createNames(packageVersions) {
|
|
7
|
+
return { imports: new Map(), packageVersions };
|
|
8
|
+
}
|
|
9
|
+
// A string, as a literal: `<` escaped, so no `</script>` or `<!--` appears
|
|
10
|
+
// when the bundle is inlined in a page, and the line and paragraph separators,
|
|
11
|
+
// which JSON leaves bare and not every engine reads inside a string.
|
|
12
|
+
export function string(value) {
|
|
13
|
+
return JSON.stringify(value).replace(/[<\u2028\u2029]/g, (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
14
|
+
}
|
|
15
|
+
export function literal(value) {
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
return string(value);
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "number") {
|
|
20
|
+
if (!Number.isFinite(value)) {
|
|
21
|
+
throw new Error(`${value} has no literal`);
|
|
22
|
+
}
|
|
23
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
24
|
+
}
|
|
25
|
+
return String(value);
|
|
26
|
+
}
|
|
27
|
+
export const undefinedValue = "void 0";
|
|
28
|
+
export function call(callee, args) {
|
|
29
|
+
return `${callee}(${args.join(", ")})`;
|
|
30
|
+
}
|
|
31
|
+
// The body in parentheses, so one that is an object literal is not read as a
|
|
32
|
+
// block.
|
|
33
|
+
export function arrow(params, body) {
|
|
34
|
+
return `(${params.join(", ")}) => (${body})`;
|
|
35
|
+
}
|
|
36
|
+
export function thunk(body) {
|
|
37
|
+
return arrow([], body);
|
|
38
|
+
}
|
|
39
|
+
export function array(elements) {
|
|
40
|
+
return `[${elements.join(", ")}]`;
|
|
41
|
+
}
|
|
42
|
+
/** An object literal, each key bare where it can be and a string where not. */
|
|
43
|
+
export function object(entries) {
|
|
44
|
+
const members = entries.map(([key, value]) => `${member(key)}: ${value}`);
|
|
45
|
+
return `{ ${members.join(", ")} }`;
|
|
46
|
+
}
|
|
47
|
+
// A key as an object literal writes it. `__proto__`, bare or quoted, would set
|
|
48
|
+
// the object's prototype instead, so a key a user may have chosen is computed,
|
|
49
|
+
// which makes it the object's own.
|
|
50
|
+
function member(key) {
|
|
51
|
+
if (key === "__proto__") {
|
|
52
|
+
return `[${string(key)}]`;
|
|
53
|
+
}
|
|
54
|
+
return IDENTIFIER.test(key) ? key : string(key);
|
|
55
|
+
}
|
|
56
|
+
// The package a module is in: its specifier's first segment, or first two
|
|
57
|
+
// when scoped (`solid-js/web` → `solid-js`, `@scope/pkg/x` → `@scope/pkg`).
|
|
58
|
+
function packageOf(from) {
|
|
59
|
+
const segments = from.split("/");
|
|
60
|
+
return segments.slice(0, from.startsWith("@") ? 2 : 1).join("/");
|
|
61
|
+
}
|
|
62
|
+
/** An export of a module the client provides, imported by the bundle. */
|
|
63
|
+
export function imported(names, { from, name, version }) {
|
|
64
|
+
// Where a script's import is written: a plugin's own (a framework's compile
|
|
65
|
+
// step) are the plugin's, trusted as it is.
|
|
66
|
+
const pkg = packageOf(from);
|
|
67
|
+
const provided = names.packageVersions[pkg];
|
|
68
|
+
if (provided === undefined) {
|
|
69
|
+
const packages = Object.entries(names.packageVersions).map(([provided, at]) => `${provided}@${at}`);
|
|
70
|
+
throw new Error(`Can't import \`${name}\` from "${from}": the client provides ` +
|
|
71
|
+
`${packages.join(", ") || "no packages"}.`);
|
|
72
|
+
}
|
|
73
|
+
if (!semver.satisfies(provided, version)) {
|
|
74
|
+
throw new Error(`Can't import \`${name}\` from "${from}": it needs ${pkg}@${version}, ` +
|
|
75
|
+
`and the client provides ${pkg}@${provided}.`);
|
|
76
|
+
}
|
|
77
|
+
const key = `${from}\0${name}`;
|
|
78
|
+
let entry = names.imports.get(key);
|
|
79
|
+
if (entry === undefined) {
|
|
80
|
+
entry = { from, name, local: `$i${names.imports.size}` };
|
|
81
|
+
names.imports.set(key, entry);
|
|
82
|
+
}
|
|
83
|
+
return entry.local;
|
|
84
|
+
}
|
|
85
|
+
/** An import declaration of one export under its local name. */
|
|
86
|
+
export function importDeclaration(from, name, local) {
|
|
87
|
+
const exported = IDENTIFIER.test(name) ? name : string(name);
|
|
88
|
+
return `import { ${exported} as ${local} } from ${string(from)};`;
|
|
89
|
+
}
|
|
90
|
+
// The same import as CommonJS writes it: `const { name: local } = require(…);`.
|
|
91
|
+
export function requireDeclaration(from, name, local) {
|
|
92
|
+
const exported = IDENTIFIER.test(name) ? name : string(name);
|
|
93
|
+
return `const { ${exported}: ${local} } = require(${string(from)});`;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code.js","sourceRoot":"","sources":["../../src/print/code.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BundleTree } from "../bundle/buildBundle.js";
|
|
2
|
+
/**
|
|
3
|
+
* A bundle tree as a module whose default export is the tree's root: the value
|
|
4
|
+
* bundled, as its scripts wrote it.
|
|
5
|
+
*
|
|
6
|
+
* The module is its imports, a module table of its scripts, each compiled for
|
|
7
|
+
* its framework when its host was built, and the root. Its imports are the
|
|
8
|
+
* client's to resolve. As an ES module (`"es"`), a page resolves them through
|
|
9
|
+
* an import map; as CommonJS (`"cjs"`), whatever runs it hands it `require`,
|
|
10
|
+
* as a React Native app does for the packages it was built with.
|
|
11
|
+
*
|
|
12
|
+
* Its map leads into the host files its scripts were written in: an index
|
|
13
|
+
* map, each script's own map a section where the script stands. What the
|
|
14
|
+
* bundler wrote around the scripts maps to nothing, since no source wrote it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function printBundle(tree: BundleTree, format: "es" | "cjs"): {
|
|
17
|
+
code: string;
|
|
18
|
+
map: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { importDeclaration, requireDeclaration, string } from "./code.js";
|
|
2
|
+
const MODULE_ID = "bundle.js";
|
|
3
|
+
// A module table's `require`: a module's exports, its entry run the first time
|
|
4
|
+
// it is asked for.
|
|
5
|
+
const RUNTIME = `const $require = (id) => {
|
|
6
|
+
if (!(id in $exports)) {
|
|
7
|
+
const module = { exports: {} };
|
|
8
|
+
$exports[id] = module.exports;
|
|
9
|
+
$modules[id](module, module.exports, $require);
|
|
10
|
+
$exports[id] = module.exports;
|
|
11
|
+
}
|
|
12
|
+
return $exports[id];
|
|
13
|
+
};`;
|
|
14
|
+
/**
|
|
15
|
+
* A bundle tree as a module whose default export is the tree's root: the value
|
|
16
|
+
* bundled, as its scripts wrote it.
|
|
17
|
+
*
|
|
18
|
+
* The module is its imports, a module table of its scripts, each compiled for
|
|
19
|
+
* its framework when its host was built, and the root. Its imports are the
|
|
20
|
+
* client's to resolve. As an ES module (`"es"`), a page resolves them through
|
|
21
|
+
* an import map; as CommonJS (`"cjs"`), whatever runs it hands it `require`,
|
|
22
|
+
* as a React Native app does for the packages it was built with.
|
|
23
|
+
*
|
|
24
|
+
* Its map leads into the host files its scripts were written in: an index
|
|
25
|
+
* map, each script's own map a section where the script stands. What the
|
|
26
|
+
* bundler wrote around the scripts maps to nothing, since no source wrote it.
|
|
27
|
+
*/
|
|
28
|
+
export function printBundle(tree, format) {
|
|
29
|
+
const { names } = tree;
|
|
30
|
+
const module = new ModuleWriter();
|
|
31
|
+
if (format === "cjs") {
|
|
32
|
+
// An ES module is strict code, and so are the scripts; CommonJS isn't.
|
|
33
|
+
module.line('"use strict";');
|
|
34
|
+
}
|
|
35
|
+
const declaration = format === "es" ? importDeclaration : requireDeclaration;
|
|
36
|
+
for (const { from, name, local } of names.imports.values()) {
|
|
37
|
+
module.line(declaration(from, name, local));
|
|
38
|
+
}
|
|
39
|
+
// Each script a module table's entry, as webpack's and Metro's are: its body
|
|
40
|
+
// under its id, run once by `$require`. The modules they require are the
|
|
41
|
+
// client's, each imported whole, once.
|
|
42
|
+
const modules = new Map(tree.modules.map(([, entry]) => [entry.id, entry]));
|
|
43
|
+
const dependencies = [
|
|
44
|
+
...new Set([...modules.values()].flatMap((entry) => entry.dependencies)),
|
|
45
|
+
];
|
|
46
|
+
dependencies.forEach((specifier, index) => module.line(format === "es"
|
|
47
|
+
? `import * as $module${index} from ${string(specifier)};`
|
|
48
|
+
: `const $module${index} = require(${string(specifier)});`));
|
|
49
|
+
module.line("const $modules = {");
|
|
50
|
+
for (const [id, entry] of modules) {
|
|
51
|
+
module.write(`${string(id)}: `);
|
|
52
|
+
module.script(entry);
|
|
53
|
+
module.line(",");
|
|
54
|
+
}
|
|
55
|
+
module.line("};");
|
|
56
|
+
module.line("const $exports = {");
|
|
57
|
+
dependencies.forEach((specifier, index) => module.line(`${string(specifier)}: $module${index},`));
|
|
58
|
+
module.line("};");
|
|
59
|
+
module.line(RUNTIME);
|
|
60
|
+
for (const [label, entry] of tree.modules) {
|
|
61
|
+
module.line(`const ${label} = $require(${string(entry.id)}).default;`);
|
|
62
|
+
}
|
|
63
|
+
// Parenthesized, so a root that is a function isn't a declaration.
|
|
64
|
+
module.write(format === "es"
|
|
65
|
+
? `export default (${tree.root});`
|
|
66
|
+
: `module.exports = (${tree.root});`);
|
|
67
|
+
return { code: module.code, map: module.map() };
|
|
68
|
+
}
|
|
69
|
+
// The module's code as it is written, and a map of the scripts in it.
|
|
70
|
+
class ModuleWriter {
|
|
71
|
+
code = "";
|
|
72
|
+
#line = 0;
|
|
73
|
+
#column = 0;
|
|
74
|
+
#sections = [];
|
|
75
|
+
write(text) {
|
|
76
|
+
this.code += text;
|
|
77
|
+
const lines = text.split("\n");
|
|
78
|
+
if (lines.length > 1) {
|
|
79
|
+
this.#line += lines.length - 1;
|
|
80
|
+
this.#column = lines[lines.length - 1].length;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.#column += text.length;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
line(text) {
|
|
87
|
+
this.write(`${text}\n`);
|
|
88
|
+
}
|
|
89
|
+
// A script's code, its map a section of the module's where it starts.
|
|
90
|
+
script({ code, map }) {
|
|
91
|
+
this.#sections.push({
|
|
92
|
+
offset: { line: this.#line, column: this.#column },
|
|
93
|
+
map: JSON.parse(map),
|
|
94
|
+
});
|
|
95
|
+
this.write(code);
|
|
96
|
+
}
|
|
97
|
+
// An index map: each script's own map, at its offset, decoded by no one here.
|
|
98
|
+
map() {
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
version: 3,
|
|
101
|
+
file: MODULE_ID,
|
|
102
|
+
sections: this.#sections,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=printBundle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"printBundle.js","sourceRoot":"","sources":["../../src/print/printBundle.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtickjs/bundler",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Runs Backtick server components and bundles a screen per request: one JavaScript file for the client",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"backtick",
|
|
7
|
+
"react-native",
|
|
8
|
+
"server-driven-ui",
|
|
9
|
+
"server-components",
|
|
10
|
+
"bundler",
|
|
11
|
+
"bundle",
|
|
12
|
+
"jsx"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://backtickjs.com",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/backtickjs/backtick/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/backtickjs/backtick.git",
|
|
21
|
+
"directory": "packages/bundler"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Jean-François Bisson <jf@backtickjs.com>",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"!dist/._cache",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"semver": "^7.8.1",
|
|
43
|
+
"@backtickjs/core": "0.1.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/semver": "^7.8.0",
|
|
47
|
+
"prettier": "^3.6.2",
|
|
48
|
+
"typescript": "^6.0.3"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -b",
|
|
52
|
+
"dev": "tsc -b --watch",
|
|
53
|
+
"format": "prettier --write --ignore-unknown .",
|
|
54
|
+
"format:check": "prettier --check --ignore-unknown .",
|
|
55
|
+
"test": "node --test \"test/**/*.test.ts\"",
|
|
56
|
+
"clean": "rm -rf node_modules dist .turbo"
|
|
57
|
+
}
|
|
6
58
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// A binding key as the compiler writes it, `<name>$<fileHash>$<n>`. Shared by
|
|
2
|
+
// the two halves of the bundler because both have to agree on what a key means,
|
|
3
|
+
// and neither owns it.
|
|
4
|
+
|
|
5
|
+
// The source name a key was written under, with the uniqueness suffix dropped.
|
|
6
|
+
// Printing it is the caller's job: two distinct keys can share a source name,
|
|
7
|
+
// and whether that matters depends on whether they can meet in one scope.
|
|
8
|
+
export function sourceName(key: string): string {
|
|
9
|
+
return key.replace(/\$[0-9a-z]+\$\d+$/, "");
|
|
10
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ClientModule,
|
|
3
|
+
type ClientScript,
|
|
4
|
+
isClientScript,
|
|
5
|
+
isJsxElement,
|
|
6
|
+
type JsxElement,
|
|
7
|
+
} from "@backtickjs/core";
|
|
8
|
+
import { isClientImport, type Spliceable } from "@backtickjs/core";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type ElementExpansions,
|
|
12
|
+
expandJsxElement,
|
|
13
|
+
} from "./expandJsxElement.js";
|
|
14
|
+
import { bindingsOf, capturesOf } from "./params.js";
|
|
15
|
+
import { sourceName } from "./bindingKey.js";
|
|
16
|
+
import {
|
|
17
|
+
array,
|
|
18
|
+
arrow,
|
|
19
|
+
call,
|
|
20
|
+
createNames,
|
|
21
|
+
imported,
|
|
22
|
+
literal,
|
|
23
|
+
object,
|
|
24
|
+
thunk,
|
|
25
|
+
undefinedValue,
|
|
26
|
+
} from "../print/code.js";
|
|
27
|
+
import type { Names } from "../print/code.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A bundle as it is built, before it is printed: each script's module it
|
|
31
|
+
* declares under its label, in the order rendering first reached it, and the
|
|
32
|
+
* root, as code; and what they import.
|
|
33
|
+
*/
|
|
34
|
+
export interface BundleTree {
|
|
35
|
+
readonly modules: readonly (readonly [string, ClientModule])[];
|
|
36
|
+
readonly root: string;
|
|
37
|
+
readonly names: Names;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Names a binding may not take: what strict code cannot bind. Every name the
|
|
41
|
+
// bundle introduces starts with `$`, which a script cannot bind, so none of
|
|
42
|
+
// those can meet a binding.
|
|
43
|
+
const RESERVED = ["arguments", "await", "eval", "yield"];
|
|
44
|
+
|
|
45
|
+
// What encloses a value being rendered: the bindings the thunks around it were
|
|
46
|
+
// handed.
|
|
47
|
+
interface Scope {
|
|
48
|
+
readonly bindings: ReadonlySet<string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rootScope: Scope = { bindings: new Set() };
|
|
52
|
+
|
|
53
|
+
// Builds the bundle `{ scripts, root }` as code, and documents how it is
|
|
54
|
+
// derived.
|
|
55
|
+
//
|
|
56
|
+
// Each script is declared once, as its compiled code under a label `$cs<n>`,
|
|
57
|
+
// and called wherever it is used. Its code takes a parameter per splice —
|
|
58
|
+
// a hole reads `$splice<i>()` — then per tag and per capture, so it is a function of the
|
|
59
|
+
// script's source alone: every call passes its own splice arguments, as
|
|
60
|
+
// thunks, and nothing from a call site is inlined.
|
|
61
|
+
//
|
|
62
|
+
// A captured variable is threaded, not resolved by name at the splice site: a
|
|
63
|
+
// fragment written in one script but spliced (via host code) into another still
|
|
64
|
+
// refers to the binding it was written under. Each script receives its live
|
|
65
|
+
// captures as parameters, and a call of it passes those captures from the
|
|
66
|
+
// enclosing scope. Because the compiler gives every binding a globally unique
|
|
67
|
+
// name, a capture is threaded under that one name the whole way down — an
|
|
68
|
+
// intermediate script that binds a same-looking variable has a different
|
|
69
|
+
// unique name, so there is nothing to disambiguate and nothing to rename.
|
|
70
|
+
export async function buildBundle(
|
|
71
|
+
value: Spliceable,
|
|
72
|
+
packageVersions: Readonly<Record<string, string>>,
|
|
73
|
+
): Promise<BundleTree> {
|
|
74
|
+
const names = createNames(packageVersions);
|
|
75
|
+
const elementExpansions: ElementExpansions = new WeakMap();
|
|
76
|
+
// Each script's number, in the order rendering first reaches it. Two
|
|
77
|
+
// scripts written at one source location are one declaration, so the first
|
|
78
|
+
// script with an id stands for all of them, and a reference to a shared
|
|
79
|
+
// script is a reference to the same object. Keyed by that script so a label
|
|
80
|
+
// is a lookup rather than a scan, and ordered by insertion, which is the
|
|
81
|
+
// order the tail declares them in.
|
|
82
|
+
const numbers = new Map<ClientScript, number>();
|
|
83
|
+
const scriptById = new Map<string, ClientScript>();
|
|
84
|
+
const scriptFor = (script: ClientScript): ClientScript => {
|
|
85
|
+
const existing = scriptById.get(script.module.id);
|
|
86
|
+
if (existing !== undefined) {
|
|
87
|
+
return existing;
|
|
88
|
+
}
|
|
89
|
+
scriptById.set(script.module.id, script);
|
|
90
|
+
numbers.set(script, numbers.size);
|
|
91
|
+
return script;
|
|
92
|
+
};
|
|
93
|
+
// A binding key printed under its source name, with a numeric suffix when two
|
|
94
|
+
// distinct bindings would otherwise print the same.
|
|
95
|
+
//
|
|
96
|
+
// Disambiguated at all because these become identifiers, and identifiers nest:
|
|
97
|
+
// a hole inside a thunk puts one thunk's parameters inside another's, so two
|
|
98
|
+
// bindings sharing a source name can land in one chain and the inner would
|
|
99
|
+
// shadow what the outer was handed (`shadowing` nests three).
|
|
100
|
+
//
|
|
101
|
+
// One scope, which is the bundle root: what a drawing is written into now
|
|
102
|
+
// that structure goes where it stands. A declared script's names are the
|
|
103
|
+
// compiler's, from its own source — nothing out here reads those names,
|
|
104
|
+
// because a call hands a script its arguments positionally, and its
|
|
105
|
+
// captures arrive as numbered parameters rather than under a name.
|
|
106
|
+
const displayed = new Map<string, string>();
|
|
107
|
+
const used = new Set<string>(RESERVED);
|
|
108
|
+
const displayName = (key: string): string => {
|
|
109
|
+
const existing = displayed.get(key);
|
|
110
|
+
if (existing !== undefined) {
|
|
111
|
+
return existing;
|
|
112
|
+
}
|
|
113
|
+
const base = sourceName(key);
|
|
114
|
+
let name = base;
|
|
115
|
+
for (let n = 2; used.has(name); n++) {
|
|
116
|
+
name = `${base}${n}`;
|
|
117
|
+
}
|
|
118
|
+
used.add(name);
|
|
119
|
+
displayed.set(key, name);
|
|
120
|
+
return name;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// The script-declared bindings a hole feeds its thunk, so a spliced fragment
|
|
124
|
+
// sees the bindings in scope at its hole even though the thunk is written at
|
|
125
|
+
// the call site. The body's hole call and every thunk's parameter list read
|
|
126
|
+
// this, so they agree positionally.
|
|
127
|
+
//
|
|
128
|
+
// Read off the script's own source (see `splices` in `resolveBindings`),
|
|
129
|
+
// not off what the arguments reaching that hole in this bundle happen to
|
|
130
|
+
// capture. That is what lets a script be compiled from its source alone: a
|
|
131
|
+
// second call site appearing elsewhere in a render cannot change a thunk a
|
|
132
|
+
// first one already had.
|
|
133
|
+
//
|
|
134
|
+
// It is a superset — the bindings a fragment written there *could* name, not
|
|
135
|
+
// the ones it does — because which fragment reaches a hole is a host
|
|
136
|
+
// decision. A carried fragment arrives with its own captures already bound,
|
|
137
|
+
// so the extra parameters are unused rather than wrong.
|
|
138
|
+
const passKeys = (target: ClientScript, hole: number): readonly string[] =>
|
|
139
|
+
bindingsOf(target.module.params[hole]);
|
|
140
|
+
|
|
141
|
+
// The scripts the bundle declares: those a reference calls or passes, not
|
|
142
|
+
// every one looked up on the way.
|
|
143
|
+
const declared = new Set<ClientScript>();
|
|
144
|
+
|
|
145
|
+
// A declared script's label: `$cs` and its number.
|
|
146
|
+
const labelOf = (target: ClientScript): string =>
|
|
147
|
+
`$cs${numbers.get(target)!}`;
|
|
148
|
+
|
|
149
|
+
const declare = (script: ClientScript): void => {
|
|
150
|
+
declared.add(script);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// A fragment that is one script whose parameters are exactly what this hole
|
|
154
|
+
// passes, so calling it is what a thunk around it would have done. The lists
|
|
155
|
+
// are compared rather than assumed: a hole hands over what its own script has,
|
|
156
|
+
// and a fragment wants what its own script needs, and those coincide often
|
|
157
|
+
// but not always.
|
|
158
|
+
const forwarding = (
|
|
159
|
+
value: Spliceable,
|
|
160
|
+
passed: readonly string[],
|
|
161
|
+
): string | null => {
|
|
162
|
+
if (
|
|
163
|
+
!isClientScript(value) ||
|
|
164
|
+
value.module.params.some((param) => param.kind !== "capture")
|
|
165
|
+
) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const target = scriptFor(value);
|
|
169
|
+
const wanted = capturesOf(target);
|
|
170
|
+
if (
|
|
171
|
+
wanted.length !== passed.length ||
|
|
172
|
+
wanted.some((key, at) => key !== passed[at])
|
|
173
|
+
) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
declare(target);
|
|
177
|
+
return labelOf(target);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Writes a value as the code it becomes: composition as data, which is what
|
|
181
|
+
// a bundle is. A script reference is a call naming which script and
|
|
182
|
+
// what to hand it; everything else is its literal form. Rendered in order,
|
|
183
|
+
// one value after the other, so the table follows the order rendering first
|
|
184
|
+
// reached each script.
|
|
185
|
+
const render = async (
|
|
186
|
+
value: Spliceable,
|
|
187
|
+
scope: Scope = rootScope,
|
|
188
|
+
): Promise<string> => {
|
|
189
|
+
if (isClientScript(value)) {
|
|
190
|
+
const target = scriptFor(value);
|
|
191
|
+
declare(target);
|
|
192
|
+
return call(labelOf(target), await exprCallArgs(value, scope));
|
|
193
|
+
}
|
|
194
|
+
if (isJsxElement(value)) {
|
|
195
|
+
return renderJsx(value, scope);
|
|
196
|
+
}
|
|
197
|
+
if (isClientImport(value)) {
|
|
198
|
+
return imported(names, value);
|
|
199
|
+
}
|
|
200
|
+
if (value === null) {
|
|
201
|
+
return literal(null);
|
|
202
|
+
}
|
|
203
|
+
// Checked by name, since everything past here reads the value as an object.
|
|
204
|
+
if (value === undefined) {
|
|
205
|
+
return undefinedValue;
|
|
206
|
+
}
|
|
207
|
+
if (
|
|
208
|
+
typeof value === "number" ||
|
|
209
|
+
typeof value === "string" ||
|
|
210
|
+
typeof value === "boolean"
|
|
211
|
+
) {
|
|
212
|
+
return literal(value);
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(value)) {
|
|
215
|
+
const elements: string[] = [];
|
|
216
|
+
for (const element of value) {
|
|
217
|
+
elements.push(await render(element, scope));
|
|
218
|
+
}
|
|
219
|
+
return array(elements);
|
|
220
|
+
}
|
|
221
|
+
// A host function is host code, which never reaches the client: client
|
|
222
|
+
// code is written in a script, and reaches another as one.
|
|
223
|
+
// A server component among them, written as a tag: `<$Rule />`.
|
|
224
|
+
if (typeof value === "function") {
|
|
225
|
+
const { name } = value as (...args: never) => unknown;
|
|
226
|
+
throw new Error(
|
|
227
|
+
(name === ""
|
|
228
|
+
? "Can't splice a host function"
|
|
229
|
+
: `Can't splice the host function \`${name}\``) +
|
|
230
|
+
": it's host code, and only runs on the host. Write a client " +
|
|
231
|
+
"function as a script instead: cs`(n: number) => ...`; a server " +
|
|
232
|
+
`component is drawn in a braced splice: \`{\${<${name || "Name"} />}}\`.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
// Only plain objects cross structurally. A class instance would land here
|
|
236
|
+
// and half-work — own fields would cross, getters and methods silently
|
|
237
|
+
// vanish — so fail loudly instead. An object with behaviour is built by a
|
|
238
|
+
// client function: `state` for what it holds, arrows for what may be done
|
|
239
|
+
// to it.
|
|
240
|
+
const prototype = Object.getPrototypeOf(value);
|
|
241
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
242
|
+
const name = value.constructor?.name ?? "an unknown class";
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Can't splice this \`${name}\` instance: only plain objects cross into ` +
|
|
245
|
+
"a client script. Build one with a client function instead.",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const entries: [string, string][] = [];
|
|
249
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
250
|
+
entries.push([key, await render(entry, scope)]);
|
|
251
|
+
}
|
|
252
|
+
return object(entries);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Renders a capture in JSON position: a parameter of an enclosing thunk
|
|
256
|
+
// resolves by name; anything else must be a parameter of the enclosing script.
|
|
257
|
+
// At
|
|
258
|
+
// the bundle root there is no enclosing instance, so a capture reaching it
|
|
259
|
+
// can't be threaded from anywhere.
|
|
260
|
+
const capExpr = (key: string, scope: Scope): string => {
|
|
261
|
+
if (scope.bindings.has(key)) {
|
|
262
|
+
return displayName(key);
|
|
263
|
+
}
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Can't thread the capture \`${sourceName(key)}\`: nothing encloses ` +
|
|
266
|
+
"this reference to supply it. A fragment carries the bindings it was " +
|
|
267
|
+
"written under, so this is also what happens when one is spliced " +
|
|
268
|
+
"somewhere another `" +
|
|
269
|
+
sourceName(key) +
|
|
270
|
+
"` shadows it: the binding is still there, but no longer reachable by " +
|
|
271
|
+
"name, and naming it anyway would mean emitting what the source " +
|
|
272
|
+
"couldn't say.",
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// The arguments of a call of a declared script: for
|
|
277
|
+
// one `#thunk` per splice ahead of the environment.
|
|
278
|
+
const exprCallArgs = async (
|
|
279
|
+
ref: ClientScript,
|
|
280
|
+
scope: Scope,
|
|
281
|
+
): Promise<string[]> => {
|
|
282
|
+
const target = scriptFor(ref);
|
|
283
|
+
const parts: string[] = [];
|
|
284
|
+
// The splices lead the parameters, one `arg` each.
|
|
285
|
+
for (const [index, arg] of ref.args.entries()) {
|
|
286
|
+
// What the hole hands over, in the order the script fixes: the bindings
|
|
287
|
+
// bound there, then the captures it forwards on behalf of whatever is
|
|
288
|
+
// nested inside it.
|
|
289
|
+
const passed = [...passKeys(target, index), ...capturesOf(target)];
|
|
290
|
+
// A fragment whose own parameters are exactly that list reads the hole's
|
|
291
|
+
// arguments as they arrive, so it is passed as it is rather than wrapped
|
|
292
|
+
// in a thunk that would only pass them along.
|
|
293
|
+
const forwarded = forwarding(arg, passed);
|
|
294
|
+
if (forwarded !== null) {
|
|
295
|
+
parts.push(forwarded);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (passed.length === 0) {
|
|
299
|
+
parts.push(thunk(await render(arg, scope)));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
// Otherwise a thunk names them and calls the fragment with what it wants.
|
|
303
|
+
const inner = { bindings: new Set([...scope.bindings, ...passed]) };
|
|
304
|
+
parts.push(arrow(passed.map(displayName), await render(arg, inner)));
|
|
305
|
+
}
|
|
306
|
+
for (const key of capturesOf(target)) {
|
|
307
|
+
parts.push(capExpr(key, scope));
|
|
308
|
+
}
|
|
309
|
+
return parts;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// A tag. A component runs on the host and what it drew stands where the tag
|
|
313
|
+
// stood; an element is its own name, each prop an expression in the
|
|
314
|
+
// enclosing script's scope.
|
|
315
|
+
//
|
|
316
|
+
// A prop that is `undefined` is left out, as JSX and TypeScript's optional
|
|
317
|
+
// props read it: a component forwarding an optional prop it wasn't given
|
|
318
|
+
// writes nothing.
|
|
319
|
+
const renderJsx = async (jsx: JsxElement, scope: Scope): Promise<string> => {
|
|
320
|
+
const type = jsx.type;
|
|
321
|
+
if (typeof type === "function" && !isClientImport(type)) {
|
|
322
|
+
return render(
|
|
323
|
+
await expandJsxElement(jsx, type, elementExpansions),
|
|
324
|
+
scope,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
// An element and a client component (an import, or a script answering
|
|
328
|
+
// one) are client code, so on the host they can't be tags: they are tags
|
|
329
|
+
// in a script.
|
|
330
|
+
if (typeof type === "string") {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`\`<${type}>\` is drawn by the client, so it belongs in a script: ` +
|
|
333
|
+
`cs\`<${type}>…</${type}>\`.`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const tag = isClientImport(type) ? `\`<${type.name}>\`` : "A script";
|
|
337
|
+
throw new Error(
|
|
338
|
+
`${tag} is a client component, so it can't be a tag on the host. ` +
|
|
339
|
+
"Use it as a tag in a script.",
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Nothing encloses the root, so nothing it holds can capture.
|
|
344
|
+
const root = await render(value as Spliceable);
|
|
345
|
+
// In table order, which is the order rendering first reached each script.
|
|
346
|
+
const modules: (readonly [string, ClientModule])[] = [];
|
|
347
|
+
for (const script of numbers.keys()) {
|
|
348
|
+
if (declared.has(script)) {
|
|
349
|
+
modules.push([labelOf(script), script.module]);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { modules, root, names };
|
|
353
|
+
}
|