@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,26 @@
|
|
|
1
|
+
import type { JsxElement, Spliceable } from "@backtickjs/core";
|
|
2
|
+
|
|
3
|
+
// The in-flight promise, so two references to one element run its component
|
|
4
|
+
// once. Per bundle, like a function's expansion: an element held at module
|
|
5
|
+
// level is the same object in every bundle, and what its component drew for
|
|
6
|
+
// one request (the request's user) is not the next one's.
|
|
7
|
+
export type ElementExpansions = WeakMap<JsxElement, Promise<Spliceable>>;
|
|
8
|
+
|
|
9
|
+
// Runs a host component and answers with what it drew. The component itself
|
|
10
|
+
// never leaves the host: a server component expands away here, and only the
|
|
11
|
+
// client component it bottoms out in reaches the bundle.
|
|
12
|
+
export function expandJsxElement(
|
|
13
|
+
jsx: JsxElement,
|
|
14
|
+
component: (props: never) => unknown,
|
|
15
|
+
expansions: ElementExpansions,
|
|
16
|
+
): Promise<Spliceable> {
|
|
17
|
+
const shared = expansions.get(jsx);
|
|
18
|
+
if (shared) {
|
|
19
|
+
return shared;
|
|
20
|
+
}
|
|
21
|
+
const expansion = Promise.resolve(
|
|
22
|
+
component(jsx.props as never) as Spliceable | Promise<Spliceable>,
|
|
23
|
+
);
|
|
24
|
+
expansions.set(jsx, expansion);
|
|
25
|
+
return expansion;
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClientScript, Param } from "@backtickjs/core";
|
|
2
|
+
|
|
3
|
+
// What a script's module says about its parameters, the same for every run.
|
|
4
|
+
|
|
5
|
+
// The bindings a hole hands over. A capture hands over none.
|
|
6
|
+
export function bindingsOf(param: Param | undefined): readonly string[] {
|
|
7
|
+
return param?.kind === "splice" ? param.bindings : [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// The binding keys a script captures, in parameter order.
|
|
11
|
+
export function capturesOf(script: ClientScript): readonly string[] {
|
|
12
|
+
return script.module.params.flatMap((param) =>
|
|
13
|
+
param.kind === "capture" ? [param.key] : [],
|
|
14
|
+
);
|
|
15
|
+
}
|
package/src/bundler.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Spliceable } from "@backtickjs/core";
|
|
2
|
+
import { buildBundle } from "./bundle/buildBundle.js";
|
|
3
|
+
import { printBundle } from "./print/printBundle.js";
|
|
4
|
+
|
|
5
|
+
export interface BuildOptions {
|
|
6
|
+
// What to bundle.
|
|
7
|
+
readonly input: Spliceable;
|
|
8
|
+
// The packages the client provides, each at its exact version: left out of
|
|
9
|
+
// the bundle, which requires them. A script's import from any other, or
|
|
10
|
+
// needing a version it doesn't have, throws.
|
|
11
|
+
readonly packageVersions: Readonly<Record<string, string>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OutputOptions {
|
|
15
|
+
// `"es"`, an ES module, for a page; `"cjs"`, CommonJS, for a client that
|
|
16
|
+
// hands the bundle `require` itself, as a React Native app does.
|
|
17
|
+
readonly format: "es" | "cjs";
|
|
18
|
+
// A map into the host files, as Rollup's: `"inline"` appends the map to the
|
|
19
|
+
// code as a `data:` URL, and `"hidden"` answers it alone. None by default.
|
|
20
|
+
// It carries host files' names and lines, not their content, which stays
|
|
21
|
+
// the server's.
|
|
22
|
+
readonly sourcemap?: false | "inline" | "hidden";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What `generate` writes: the code, and its map where one was asked for. */
|
|
26
|
+
export interface OutputChunk {
|
|
27
|
+
readonly code: string;
|
|
28
|
+
readonly map: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A built bundle, written out by `generate`. */
|
|
32
|
+
export interface Bundle {
|
|
33
|
+
generate(options: OutputOptions): OutputChunk;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What runs your components and hands back what they drew, shaped as Rollup
|
|
38
|
+
* is: a build, then output from it.
|
|
39
|
+
*
|
|
40
|
+
* const bundle = await bundler.build({ input: <Home />, packageVersions });
|
|
41
|
+
* const { code, map } = bundle.generate({ format: "es", sourcemap: "hidden" });
|
|
42
|
+
*
|
|
43
|
+
* The module's default export is the value (CommonJS: `module.exports`). A
|
|
44
|
+
* namespace rather than a bare function, so `bundle` stays a name a caller can
|
|
45
|
+
* give what comes back.
|
|
46
|
+
*/
|
|
47
|
+
export const bundler = {
|
|
48
|
+
async build({ input, packageVersions }: BuildOptions): Promise<Bundle> {
|
|
49
|
+
// Scripts come compiled for their framework when their host was built, so
|
|
50
|
+
// the bundle is only put together: nothing is compiled or parsed here.
|
|
51
|
+
const tree = await buildBundle(input, packageVersions);
|
|
52
|
+
return {
|
|
53
|
+
generate: ({ format, sourcemap = false }) => {
|
|
54
|
+
const { code, map } = printBundle(tree, format);
|
|
55
|
+
if (sourcemap === false) {
|
|
56
|
+
return { code, map: null };
|
|
57
|
+
}
|
|
58
|
+
if (sourcemap === "hidden") {
|
|
59
|
+
return { code, map };
|
|
60
|
+
}
|
|
61
|
+
const url = `data:application/json;charset=utf-8,${encodeURIComponent(map)}`;
|
|
62
|
+
return { code: `${code}\n//# sourceMappingURL=${url}`, map };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ClientImport } from "@backtickjs/core";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
|
|
4
|
+
// What a bundle is written as, and the one place its conventions live: how a
|
|
5
|
+
// name is written, how a literal escapes, and how an element is written as JSX
|
|
6
|
+
// for the framework's compiler. Everything is code, as a string.
|
|
7
|
+
|
|
8
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* What a bundle imports, keyed by specifier and export, each bound once; and
|
|
12
|
+
* the packages it may import them from, which the client provides, by version.
|
|
13
|
+
*/
|
|
14
|
+
export interface Names {
|
|
15
|
+
readonly imports: Map<string, { from: string; name: string; local: string }>;
|
|
16
|
+
readonly packageVersions: Readonly<Record<string, string>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createNames(
|
|
20
|
+
packageVersions: Readonly<Record<string, string>>,
|
|
21
|
+
): Names {
|
|
22
|
+
return { imports: new Map(), packageVersions };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A string, as a literal: `<` escaped, so no `</script>` or `<!--` appears
|
|
26
|
+
// when the bundle is inlined in a page, and the line and paragraph separators,
|
|
27
|
+
// which JSON leaves bare and not every engine reads inside a string.
|
|
28
|
+
export function string(value: string): string {
|
|
29
|
+
return JSON.stringify(value).replace(
|
|
30
|
+
/[<\u2028\u2029]/g,
|
|
31
|
+
(unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, "0")}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function literal(value: string | number | boolean | null): string {
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
return string(value);
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "number") {
|
|
40
|
+
if (!Number.isFinite(value)) {
|
|
41
|
+
throw new Error(`${value} has no literal`);
|
|
42
|
+
}
|
|
43
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
44
|
+
}
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const undefinedValue = "void 0";
|
|
49
|
+
|
|
50
|
+
export function call(callee: string, args: readonly string[]): string {
|
|
51
|
+
return `${callee}(${args.join(", ")})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The body in parentheses, so one that is an object literal is not read as a
|
|
55
|
+
// block.
|
|
56
|
+
export function arrow(params: readonly string[], body: string): string {
|
|
57
|
+
return `(${params.join(", ")}) => (${body})`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function thunk(body: string): string {
|
|
61
|
+
return arrow([], body);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function array(elements: readonly string[]): string {
|
|
65
|
+
return `[${elements.join(", ")}]`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** An object literal, each key bare where it can be and a string where not. */
|
|
69
|
+
export function object(
|
|
70
|
+
entries: readonly (readonly [string, string])[],
|
|
71
|
+
): string {
|
|
72
|
+
const members = entries.map(([key, value]) => `${member(key)}: ${value}`);
|
|
73
|
+
return `{ ${members.join(", ")} }`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A key as an object literal writes it. `__proto__`, bare or quoted, would set
|
|
77
|
+
// the object's prototype instead, so a key a user may have chosen is computed,
|
|
78
|
+
// which makes it the object's own.
|
|
79
|
+
function member(key: string): string {
|
|
80
|
+
if (key === "__proto__") {
|
|
81
|
+
return `[${string(key)}]`;
|
|
82
|
+
}
|
|
83
|
+
return IDENTIFIER.test(key) ? key : string(key);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The package a module is in: its specifier's first segment, or first two
|
|
87
|
+
// when scoped (`solid-js/web` → `solid-js`, `@scope/pkg/x` → `@scope/pkg`).
|
|
88
|
+
function packageOf(from: string): string {
|
|
89
|
+
const segments = from.split("/");
|
|
90
|
+
return segments.slice(0, from.startsWith("@") ? 2 : 1).join("/");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** An export of a module the client provides, imported by the bundle. */
|
|
94
|
+
export function imported(
|
|
95
|
+
names: Names,
|
|
96
|
+
{ from, name, version }: ClientImport<unknown>,
|
|
97
|
+
): string {
|
|
98
|
+
// Where a script's import is written: a plugin's own (a framework's compile
|
|
99
|
+
// step) are the plugin's, trusted as it is.
|
|
100
|
+
const pkg = packageOf(from);
|
|
101
|
+
const provided = names.packageVersions[pkg];
|
|
102
|
+
if (provided === undefined) {
|
|
103
|
+
const packages = Object.entries(names.packageVersions).map(
|
|
104
|
+
([provided, at]) => `${provided}@${at}`,
|
|
105
|
+
);
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Can't import \`${name}\` from "${from}": the client provides ` +
|
|
108
|
+
`${packages.join(", ") || "no packages"}.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (!semver.satisfies(provided, version)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Can't import \`${name}\` from "${from}": it needs ${pkg}@${version}, ` +
|
|
114
|
+
`and the client provides ${pkg}@${provided}.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const key = `${from}\0${name}`;
|
|
118
|
+
let entry = names.imports.get(key);
|
|
119
|
+
if (entry === undefined) {
|
|
120
|
+
entry = { from, name, local: `$i${names.imports.size}` };
|
|
121
|
+
names.imports.set(key, entry);
|
|
122
|
+
}
|
|
123
|
+
return entry.local;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** An import declaration of one export under its local name. */
|
|
127
|
+
export function importDeclaration(
|
|
128
|
+
from: string,
|
|
129
|
+
name: string,
|
|
130
|
+
local: string,
|
|
131
|
+
): string {
|
|
132
|
+
const exported = IDENTIFIER.test(name) ? name : string(name);
|
|
133
|
+
return `import { ${exported} as ${local} } from ${string(from)};`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The same import as CommonJS writes it: `const { name: local } = require(…);`.
|
|
137
|
+
export function requireDeclaration(
|
|
138
|
+
from: string,
|
|
139
|
+
name: string,
|
|
140
|
+
local: string,
|
|
141
|
+
): string {
|
|
142
|
+
const exported = IDENTIFIER.test(name) ? name : string(name);
|
|
143
|
+
return `const { ${exported}: ${local} } = require(${string(from)});`;
|
|
144
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { BundleTree } from "../bundle/buildBundle.js";
|
|
2
|
+
import type { ClientModule } from "@backtickjs/core";
|
|
3
|
+
import { importDeclaration, requireDeclaration, string } from "./code.js";
|
|
4
|
+
|
|
5
|
+
const MODULE_ID = "bundle.js";
|
|
6
|
+
|
|
7
|
+
// A module table's `require`: a module's exports, its entry run the first time
|
|
8
|
+
// it is asked for.
|
|
9
|
+
const RUNTIME = `const $require = (id) => {
|
|
10
|
+
if (!(id in $exports)) {
|
|
11
|
+
const module = { exports: {} };
|
|
12
|
+
$exports[id] = module.exports;
|
|
13
|
+
$modules[id](module, module.exports, $require);
|
|
14
|
+
$exports[id] = module.exports;
|
|
15
|
+
}
|
|
16
|
+
return $exports[id];
|
|
17
|
+
};`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A bundle tree as a module whose default export is the tree's root: the value
|
|
21
|
+
* bundled, as its scripts wrote it.
|
|
22
|
+
*
|
|
23
|
+
* The module is its imports, a module table of its scripts, each compiled for
|
|
24
|
+
* its framework when its host was built, and the root. Its imports are the
|
|
25
|
+
* client's to resolve. As an ES module (`"es"`), a page resolves them through
|
|
26
|
+
* an import map; as CommonJS (`"cjs"`), whatever runs it hands it `require`,
|
|
27
|
+
* as a React Native app does for the packages it was built with.
|
|
28
|
+
*
|
|
29
|
+
* Its map leads into the host files its scripts were written in: an index
|
|
30
|
+
* map, each script's own map a section where the script stands. What the
|
|
31
|
+
* bundler wrote around the scripts maps to nothing, since no source wrote it.
|
|
32
|
+
*/
|
|
33
|
+
export function printBundle(
|
|
34
|
+
tree: BundleTree,
|
|
35
|
+
format: "es" | "cjs",
|
|
36
|
+
): { code: string; map: string } {
|
|
37
|
+
const { names } = tree;
|
|
38
|
+
const module = new ModuleWriter();
|
|
39
|
+
if (format === "cjs") {
|
|
40
|
+
// An ES module is strict code, and so are the scripts; CommonJS isn't.
|
|
41
|
+
module.line('"use strict";');
|
|
42
|
+
}
|
|
43
|
+
const declaration = format === "es" ? importDeclaration : requireDeclaration;
|
|
44
|
+
for (const { from, name, local } of names.imports.values()) {
|
|
45
|
+
module.line(declaration(from, name, local));
|
|
46
|
+
}
|
|
47
|
+
// Each script a module table's entry, as webpack's and Metro's are: its body
|
|
48
|
+
// under its id, run once by `$require`. The modules they require are the
|
|
49
|
+
// client's, each imported whole, once.
|
|
50
|
+
const modules = new Map(tree.modules.map(([, entry]) => [entry.id, entry]));
|
|
51
|
+
const dependencies = [
|
|
52
|
+
...new Set([...modules.values()].flatMap((entry) => entry.dependencies)),
|
|
53
|
+
];
|
|
54
|
+
dependencies.forEach((specifier, index) =>
|
|
55
|
+
module.line(
|
|
56
|
+
format === "es"
|
|
57
|
+
? `import * as $module${index} from ${string(specifier)};`
|
|
58
|
+
: `const $module${index} = require(${string(specifier)});`,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
module.line("const $modules = {");
|
|
62
|
+
for (const [id, entry] of modules) {
|
|
63
|
+
module.write(`${string(id)}: `);
|
|
64
|
+
module.script(entry);
|
|
65
|
+
module.line(",");
|
|
66
|
+
}
|
|
67
|
+
module.line("};");
|
|
68
|
+
module.line("const $exports = {");
|
|
69
|
+
dependencies.forEach((specifier, index) =>
|
|
70
|
+
module.line(`${string(specifier)}: $module${index},`),
|
|
71
|
+
);
|
|
72
|
+
module.line("};");
|
|
73
|
+
module.line(RUNTIME);
|
|
74
|
+
for (const [label, entry] of tree.modules) {
|
|
75
|
+
module.line(`const ${label} = $require(${string(entry.id)}).default;`);
|
|
76
|
+
}
|
|
77
|
+
// Parenthesized, so a root that is a function isn't a declaration.
|
|
78
|
+
module.write(
|
|
79
|
+
format === "es"
|
|
80
|
+
? `export default (${tree.root});`
|
|
81
|
+
: `module.exports = (${tree.root});`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return { code: module.code, map: module.map() };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The module's code as it is written, and a map of the scripts in it.
|
|
88
|
+
class ModuleWriter {
|
|
89
|
+
code = "";
|
|
90
|
+
#line = 0;
|
|
91
|
+
#column = 0;
|
|
92
|
+
#sections: { offset: { line: number; column: number }; map: object }[] = [];
|
|
93
|
+
|
|
94
|
+
write(text: string): void {
|
|
95
|
+
this.code += text;
|
|
96
|
+
const lines = text.split("\n");
|
|
97
|
+
if (lines.length > 1) {
|
|
98
|
+
this.#line += lines.length - 1;
|
|
99
|
+
this.#column = lines[lines.length - 1]!.length;
|
|
100
|
+
} else {
|
|
101
|
+
this.#column += text.length;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
line(text: string): void {
|
|
106
|
+
this.write(`${text}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A script's code, its map a section of the module's where it starts.
|
|
110
|
+
script({ code, map }: ClientModule): void {
|
|
111
|
+
this.#sections.push({
|
|
112
|
+
offset: { line: this.#line, column: this.#column },
|
|
113
|
+
map: JSON.parse(map) as object,
|
|
114
|
+
});
|
|
115
|
+
this.write(code);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// An index map: each script's own map, at its offset, decoded by no one here.
|
|
119
|
+
map(): string {
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
version: 3,
|
|
122
|
+
file: MODULE_ID,
|
|
123
|
+
sections: this.#sections,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|