@statewalker/webrun-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/src/resolve.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { imports as resolveImports, exports as resolvePackageExports } from "resolve.exports";
2
+ import { dirOf, isFile, joinWithin, parsePackageSpecifier } from "./paths.js";
3
+ import type { Importer, Mount, PackageManifest, PackageStore } from "./types.js";
4
+
5
+ /** Extensions tried, in order, for a path written without one (or with `.js` for a `.ts` file). */
6
+ const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".json"];
7
+
8
+ export interface Resolved {
9
+ mount: Mount;
10
+ path: string;
11
+ }
12
+
13
+ /** `path` as written, else with an extension, else its `index` file. */
14
+ async function resolveFile(mount: Mount, path: string): Promise<string | undefined> {
15
+ if (await isFile(mount, path)) return path;
16
+ // TypeScript sources import `./x.js` for `./x.ts`.
17
+ const js = /\.(m|c)?jsx?$/.exec(path);
18
+ if (js) {
19
+ const base = path.slice(0, -js[0].length);
20
+ for (const ext of [`.${js[1] ?? ""}ts`, ".tsx"])
21
+ if (await isFile(mount, base + ext)) return base + ext;
22
+ }
23
+ for (const ext of EXTENSIONS) if (await isFile(mount, path + ext)) return path + ext;
24
+ const dir = path.endsWith("/") ? path.slice(0, -1) : path;
25
+ for (const ext of EXTENSIONS)
26
+ if (await isFile(mount, `${dir}/index${ext}`)) return `${dir}/index${ext}`;
27
+ return undefined;
28
+ }
29
+
30
+ type Conditions = { conditions: string[]; require: boolean; browser: boolean };
31
+
32
+ function exportsTarget(
33
+ manifest: PackageManifest,
34
+ entry: string,
35
+ c: Conditions,
36
+ ): string | undefined {
37
+ try {
38
+ const out = resolvePackageExports(manifest, entry, {
39
+ conditions: c.conditions,
40
+ require: c.require,
41
+ browser: c.browser,
42
+ }) as string[] | undefined;
43
+ return out?.[0];
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ /** A package's file for `subpath`: `exports` first, then `module`/`main` (and `browser` when it is a string). */
50
+ async function resolvePackageEntry(
51
+ mount: Mount,
52
+ subpath: string | undefined,
53
+ c: Conditions,
54
+ ): Promise<string | undefined> {
55
+ const manifest = mount.manifest ?? {};
56
+ const entry = subpath ? `./${subpath}` : ".";
57
+ if (manifest.exports !== undefined) {
58
+ const target = exportsTarget(manifest, entry, c);
59
+ if (!target) return undefined; // not exported: Node refuses it as well
60
+ return joinWithin(mount.root, mount.root, target);
61
+ }
62
+ if (subpath) return resolveFile(mount, joinWithin(mount.root, mount.root, subpath) ?? "");
63
+ const fields = [
64
+ ...(c.browser && typeof manifest.browser === "string" ? [manifest.browser] : []),
65
+ ...(c.require ? [] : [manifest.module]),
66
+ manifest.main,
67
+ "index.js",
68
+ ].filter((f): f is string => typeof f === "string");
69
+ for (const f of fields) {
70
+ const p = joinWithin(mount.root, mount.root, f);
71
+ const file = p && (await resolveFile(mount, p));
72
+ if (file) return file;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * Resolves `spec` as written in `importer`: a path of the same tree (`./x`, `../y.js`,
79
+ * `/z`), a `#imports` alias of the importer's package, or a package found by `packages`
80
+ * (with its `exports`, `module` and `main`).
81
+ */
82
+ export async function resolveSpecifier(
83
+ spec: string,
84
+ importer: Importer,
85
+ packages: PackageStore,
86
+ c: Conditions,
87
+ ): Promise<Resolved | undefined> {
88
+ const { mount } = importer;
89
+ if (spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec === ".") {
90
+ const base = spec.startsWith("/") ? mount.root : dirOf(importer.path);
91
+ const target = joinWithin(mount.root, base, spec.startsWith("/") ? spec.slice(1) : spec);
92
+ const path = target && (await resolveFile(mount, target));
93
+ return path ? { mount, path } : undefined;
94
+ }
95
+ if (spec.startsWith("#")) {
96
+ try {
97
+ const out = resolveImports(mount.manifest ?? {}, spec, {
98
+ conditions: c.conditions,
99
+ require: c.require,
100
+ browser: c.browser,
101
+ }) as string[] | undefined;
102
+ const target = out?.[0];
103
+ if (!target) return undefined;
104
+ if (!target.startsWith(".")) return resolveSpecifier(target, importer, packages, c);
105
+ const path = joinWithin(mount.root, mount.root, target);
106
+ return path && (await isFile(mount, path)) ? { mount, path } : undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+ const { name, subpath } = parsePackageSpecifier(spec);
112
+ const pkg = await packages.get(name, importer);
113
+ if (!pkg) return undefined;
114
+ const path = await resolvePackageEntry(pkg, subpath, c);
115
+ return path && (await isFile(pkg, path)) ? { mount: pkg, path } : undefined;
116
+ }
package/src/types.ts ADDED
@@ -0,0 +1,154 @@
1
+ import type { FilesApi } from "@statewalker/webrun-files";
2
+ import type { BuildOptions, build, Message, Metafile, transform } from "esbuild-wasm";
3
+
4
+ /**
5
+ * The esbuild API the bundler calls: `esbuild-wasm` (initialized, in browsers and
6
+ * workers) or native `esbuild` on Node. Only `build` and `transform` are used, and
7
+ * every file reaches esbuild through the bundler's plugin: esbuild never touches a
8
+ * file system itself.
9
+ */
10
+ export interface Esbuild {
11
+ build: typeof build;
12
+ transform: typeof transform;
13
+ }
14
+
15
+ /** The fields of a `package.json` the resolver reads. */
16
+ export interface PackageManifest {
17
+ name?: string;
18
+ version?: string;
19
+ exports?: unknown;
20
+ imports?: unknown;
21
+ main?: string;
22
+ module?: string;
23
+ browser?: unknown;
24
+ dependencies?: Record<string, string>;
25
+ peerDependencies?: Record<string, string>;
26
+ optionalDependencies?: Record<string, string>;
27
+ devDependencies?: Record<string, string>;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ /**
32
+ * A tree of files the bundler reads: the project, or one npm package. Its files get
33
+ * ids `<prefix><path>` (`~/src/app.ts`, `npm:react@18.3.1/index.js`); the ids are what
34
+ * the source maps name, so every bundled line traces back to the user's file or to the
35
+ * package it came from.
36
+ */
37
+ export interface Mount {
38
+ /** `~` for the project, `npm:<name>@<version>` for a package. */
39
+ prefix: string;
40
+ files: FilesApi;
41
+ /** The tree's directory in `files` (`/` for a package of its own, `/node_modules/x`). */
42
+ root: string;
43
+ /** Its `package.json`, when it has one. */
44
+ manifest?: PackageManifest;
45
+ }
46
+
47
+ /** Where an import comes from: a file of a mount. */
48
+ export interface Importer {
49
+ mount: Mount;
50
+ /** The importing file's path in `mount.files` (root included). */
51
+ path: string;
52
+ }
53
+
54
+ /**
55
+ * Finds the npm packages bare specifiers name: in `node_modules` folders of a FilesApi
56
+ * (`nodeModulesPackages`), fetched from a registry on demand (`npmPackages`), or both
57
+ * (`chainPackages`).
58
+ */
59
+ export interface PackageStore {
60
+ /** The package `name` as `importer` sees it; `undefined` when it has none. */
61
+ get(name: string, importer: Importer): Promise<Mount | undefined>;
62
+ }
63
+
64
+ /** A specifier left to the runtime (`cloudflare:workers`, `node:fs`): a string ending in `*` is a prefix. */
65
+ export type ExternalPattern = string | RegExp;
66
+
67
+ export interface BundleOptions {
68
+ /** `esbuild-wasm` (or native `esbuild` on Node). */
69
+ esbuild: Esbuild;
70
+ /** The project's files. */
71
+ files: FilesApi;
72
+ /** The project's directory in `files` (default `/`). */
73
+ root?: string;
74
+ /** The entry module, a path in the project (`/src/index.ts`). */
75
+ entry: string;
76
+ /** Where bare specifiers go (default: `node_modules` folders of `files`). */
77
+ packages?: PackageStore;
78
+ /** Specifiers kept as imports for the runtime. `http(s):` and `data:` URLs always are. */
79
+ external?: ExternalPattern[];
80
+ /**
81
+ * Modules the host supplies, by specifier (`cloudflare:workers`, `node:buffer`): their
82
+ * JavaScript source, or `undefined` to resolve the specifier as usual. They come before
83
+ * `external`. Their own imports resolve from the project's root; they name themselves
84
+ * `virtual:<specifier>` in the source map.
85
+ */
86
+ modules?: Record<string, string> | ((specifier: string) => string | undefined);
87
+ /**
88
+ * Modules whose exports replace the globals of the same name in every bundled file
89
+ * (esbuild `inject`): specifiers or project paths, resolved like imports (`modules` too).
90
+ */
91
+ inject?: string[];
92
+ /**
93
+ * `package.json` `exports` conditions besides `import`/`require` and `default`
94
+ * (default `["browser"]`; a workerd target takes `["workerd", "worker", "browser"]`).
95
+ */
96
+ conditions?: string[];
97
+ /** Output format (default `esm`). */
98
+ format?: "esm" | "iife" | "cjs";
99
+ /** esbuild platform (default `browser`): sets `process.env.NODE_ENV` handling and the like. */
100
+ platform?: "browser" | "node" | "neutral";
101
+ /** Language target (default `es2022`). */
102
+ target?: string | string[];
103
+ /**
104
+ * Lower `async`/`await`, async generators and `for await` to generators, so that each
105
+ * `await` resumes through `Promise.prototype.then` (an AsyncLocalStorage polyfill can then
106
+ * carry its context through it). Source maps follow the lowered code.
107
+ */
108
+ lowerAwait?: boolean;
109
+ minify?: boolean;
110
+ define?: Record<string, string>;
111
+ /** Name of the output file: the map is linked as `<outfile>.map` (default `bundle.js`). */
112
+ outfile?: string;
113
+ /** `external` (default): `code` links `<outfile>.map`, which `map` holds; `inline`: `code` carries it too. */
114
+ sourcemap?: "external" | "inline" | false;
115
+ /** Other esbuild options, applied last (the bundler's plugin and source maps stay). */
116
+ esbuildOptions?: Partial<BuildOptions>;
117
+ }
118
+
119
+ /** A source map (version 3). */
120
+ export interface SourceMap {
121
+ version: 3;
122
+ file?: string;
123
+ sources: string[];
124
+ sourcesContent?: (string | null)[];
125
+ names: string[];
126
+ mappings: string;
127
+ sourceRoot?: string;
128
+ }
129
+
130
+ export interface BundleResult {
131
+ code: string;
132
+ /** The bundle's source map: sources are file ids (`~/src/app.ts`, `npm:pkg@1.0.0/src/x.ts`). */
133
+ map: SourceMap;
134
+ metafile: Metafile;
135
+ warnings: Message[];
136
+ }
137
+
138
+ export interface TranspileOptions {
139
+ esbuild: Esbuild;
140
+ /** The file's id or path: names it in the source map and picks the loader by extension. */
141
+ path: string;
142
+ format?: "esm" | "iife" | "cjs";
143
+ target?: string | string[];
144
+ lowerAwait?: boolean;
145
+ minify?: boolean;
146
+ define?: Record<string, string>;
147
+ sourcemap?: "external" | "inline";
148
+ }
149
+
150
+ export interface TranspileResult {
151
+ code: string;
152
+ map: SourceMap;
153
+ warnings: Message[];
154
+ }