@mosetta/build 0.0.0-stage → 0.0.9

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.
@@ -0,0 +1,159 @@
1
+ import { path } from './nodeApi.mjs';
2
+ import { exports as resolveExports } from 'resolve.exports';
3
+ import { probeFile, isDirectoryRequest } from './probeFile.mjs';
4
+ import { packageDirOf } from './packageMeta.mjs';
5
+ import { shortHash } from './hash.mjs';
6
+
7
+ // environment harnesses (CLI, plan packer) need the upward walk — re-export it from this entry
8
+ export { packageDirOf, packageVersionOf } from './packageMeta.mjs';
9
+
10
+ /**
11
+ * Package resolution ON TOP of the file port (the "lock scan + code scan" step).
12
+ *
13
+ * Resolution used to be port methods and on node hid inside `createRequire` — i.e. an
14
+ * environment capability. Now it is a SEPARATE abstraction with a single implementation,
15
+ * identical across environments: the "(importer package, name) → package dir" edges arrive as
16
+ * DATA (a catalog from the scanner), while the in-package entry is chosen here — the `exports`
17
+ * map via `resolve.exports`, legacy main/index via `probeFile`. The resolver reads files through
18
+ * the port: it needs package.json and path probes, but has no environment magic of its own.
19
+ *
20
+ * In the CLI the catalog is generated by the scanner before every build iteration; in an isolate
21
+ * it arrives in the plan. The environment difference lives entirely in the catalog's origin; the
22
+ * resolver itself does not know about it.
23
+ */
24
+
25
+ // Files the build mixes in FROM THE TOOL'S OWN installation, not from the distributive
26
+ // (loader, bootstrap, default facets). The list lives here so that the scanner and the plan
27
+ // packer build `catalog.framework` from the same enumeration.
28
+ export const FRAMEWORK_RESOLUTIONS = [
29
+ '@mosetta/build',
30
+ '@mosetta/runtime',
31
+ '@mosetta/runtime/bootstrap',
32
+ 'systemjs/dist/system.js',
33
+ 'systemjs/dist/extras/named-register.js',
34
+ ];
35
+
36
+ const MAX_WALK_UP = 50;
37
+
38
+ const notFound = (specifier, fromDir, reason) => {
39
+ const e = new Error(`Cannot resolve '${specifier}' from '${fromDir}'${reason ? ` (${reason})` : ''}`);
40
+ e.code = 'MODULE_NOT_FOUND';
41
+ return e;
42
+ };
43
+
44
+ // '@scope/name/sub/path' → { name: '@scope/name', subpath: './sub/path' }
45
+ function splitBare(specifier) {
46
+ const parts = specifier.split('/');
47
+ const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
48
+ const rest = specifier.slice(name.length);
49
+ return { name, subpath: rest ? `.${rest}` : '.' };
50
+ }
51
+
52
+ export class PackagesResolver {
53
+ #stamp = null;
54
+
55
+ constructor({ fileSystem, catalog }) {
56
+ this.fileSystem = fileSystem;
57
+ this.catalog = catalog;
58
+ }
59
+
60
+ /**
61
+ * Catalog fingerprint — the resolution edges as a whole. The dev server rescans the catalog
62
+ * on every iteration, and `pnpm add` mid-session changes RESOLUTION without touching a single
63
+ * source file: the artifact cache needs a signal that notices this. Computed once per resolver.
64
+ */
65
+ get stamp() {
66
+ return (this.#stamp ??= shortHash(JSON.stringify(this.catalog)));
67
+ }
68
+
69
+ /**
70
+ * Resolve a specifier as seen FROM `fromDir` (importer-relative resolution). Throws on failure.
71
+ *
72
+ * `conditions` — the `exports` conditions of the TARGET environment (`['browser','import']`, …).
73
+ * This is TARGET POLICY, not an environment property: dual packages have different files on the
74
+ * `require` and `import` branches (zod: `index.cjs` vs `index.js`). `null` — require semantics
75
+ * (how node's `require()` resolves).
76
+ */
77
+ resolve(specifier, fromDir, conditions = null) {
78
+ if (specifier.startsWith('.') || path.isAbsolute(specifier)) {
79
+ const target = path.isAbsolute(specifier) ? specifier : path.resolve(fromDir, specifier);
80
+ const found = probeFile(this.fileSystem, target, { directoryOnly: isDirectoryRequest(specifier) });
81
+ if (!found) throw notFound(specifier, fromDir);
82
+ return found;
83
+ }
84
+ const { name, subpath } = splitBare(specifier);
85
+ const pkgDir = this.#packageDirFor(name, fromDir);
86
+ if (!pkgDir) throw notFound(specifier, fromDir, 'no edge in the package catalog');
87
+ return this.#entryOf(pkgDir, specifier, subpath, conditions, fromDir);
88
+ }
89
+
90
+ /**
91
+ * Resolve from the TOOL'S OWN installation, not the distributive: systemjs, `@mosetta/runtime`
92
+ * and the rest of the FRAMEWORK_RESOLUTIONS list — via precomputed catalog answers.
93
+ */
94
+ resolveFramework(specifier) {
95
+ const hit = this.catalog.framework?.[specifier];
96
+ if (!hit) throw notFound(specifier, '<framework>', 'not in catalog.framework');
97
+ return hit;
98
+ }
99
+
100
+ // "name → package dir" edge: from the package owning fromDir up through its parents — the same
101
+ // directionality as node's node_modules walk, only over recorded edges.
102
+ #packageDirFor(name, fromDir) {
103
+ let dir = fromDir;
104
+ for (let i = 0; i < MAX_WALK_UP; i++) {
105
+ const pkgDir = packageDirOf(this.fileSystem, path.join(dir, '_'));
106
+ if (!pkgDir) return null;
107
+ const dep = this.catalog.edges[pkgDir]?.[name];
108
+ if (dep) return dep;
109
+ // parity policy: product code sees only dependencies — the error explains WHAT to fix
110
+ // instead of a bare "not found"
111
+ if (this.catalog.devOnly?.[pkgDir]?.[name]) {
112
+ const e = new Error(
113
+ `'${name}' is declared in devDependencies (${pkgDir}) but imported by product code — `
114
+ + `a production build sees only dependencies; move the package to dependencies`,
115
+ );
116
+ e.code = 'DEV_DEPENDENCY';
117
+ throw e;
118
+ }
119
+ try {
120
+ // self-reference: a package importing itself by name (node allows this via exports)
121
+ if (this.fileSystem.readJson(path.join(pkgDir, 'package.json')).name === name) return pkgDir;
122
+ } catch { /* no manifest — keep walking up */ }
123
+ const parent = path.dirname(pkgDir);
124
+ if (parent === pkgDir) return null;
125
+ dir = parent;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ // Entry inside the package: the `exports` map (the modern path, with encapsulation) or legacy
131
+ // main/index/path probing. Same order as node: the presence of `exports` disables legacy.
132
+ #entryOf(pkgDir, specifier, subpath, conditions, fromDir) {
133
+ let pkg = null;
134
+ try {
135
+ pkg = this.fileSystem.readJson(path.join(pkgDir, 'package.json'));
136
+ } catch { /* package without a manifest — legacy branch */ }
137
+
138
+ if (pkg?.exports != null) {
139
+ let hit;
140
+ try {
141
+ // `unsafe: true` — do NOT add conditions on our own. Otherwise resolve.exports implicitly
142
+ // mixes in `node` and the package resolves to its node branch built for real node
143
+ // (disk reads, runtime wasm compilation) — a guaranteed failure in an isolate, and simply
144
+ // the wrong code in a browser artifact. Conditions come from TARGET POLICY
145
+ // (`conditions.mjs`); resolve.exports always traverses `default` branches.
146
+ hit = resolveExports(pkg, specifier, { conditions: conditions ?? ['require'], unsafe: true });
147
+ } catch {
148
+ throw notFound(specifier, fromDir, 'not in the exports map');
149
+ }
150
+ if (!hit?.[0]) throw notFound(specifier, fromDir, 'not in the exports map');
151
+ return path.join(pkgDir, hit[0]);
152
+ }
153
+
154
+ const target = subpath === '.' ? pkgDir : path.resolve(pkgDir, subpath);
155
+ const found = probeFile(this.fileSystem, target, { directoryOnly: isDirectoryRequest(specifier) });
156
+ if (!found) throw notFound(specifier, fromDir);
157
+ return found;
158
+ }
159
+ }
@@ -0,0 +1,41 @@
1
+ import { path } from '../nodeApi.mjs';
2
+ import { hashedAssetName } from '../hash.mjs';
3
+
4
+ export const ASSET_RE = /\.(png|jpe?g|gif|svg|webp|avif|ico|bmp|woff2?|ttf|eot|otf|mp4|webm|mp3|wav|ogg|pdf)$/i;
5
+
6
+ /**
7
+ * Rollup plugin: turn `import x from './img.png'` into a value resolved at runtime by
8
+ * @mosetta/runtime's __asset(rel, import.meta.url). Records {src,out} so the asset file can be
9
+ * copied co-located with the module. `collected` is a Map(out -> srcAbsPath) (dedup by out name).
10
+ */
11
+ export function assets(collected, extraExtensions, fileSystem) {
12
+ // data-level extensibility: `assetExtensions` from mosetta.json widens the built-in
13
+ // list — a custom asset type in the simple case needs no rollup plugin at all
14
+ const extraRe = extraExtensions?.length
15
+ ? new RegExp(`\\.(${extraExtensions.map((e) => e.replace(/^\./, '')).join('|')})$`, 'i')
16
+ : null;
17
+ const isAsset = (source) => ASSET_RE.test(source) || (extraRe && extraRe.test(source));
18
+ return {
19
+ name: 'mosetta-assets',
20
+ async resolveId(source, importer) {
21
+ if (!isAsset(source)) return null;
22
+ const abs = importer ? path.resolve(path.dirname(importer), source) : path.resolve(source);
23
+ return { id: `${abs}?mosetta-asset`, external: false };
24
+ },
25
+ load(id) {
26
+ if (!id.endsWith('?mosetta-asset')) return null;
27
+ const abs = id.slice(0, -'?mosetta-asset'.length);
28
+ const buf = fileSystem.readFile(abs);
29
+ // the asset's name derives from its CONTENT: editing an image changes the name inside the
30
+ // artifact, so the file itself must be watched (the id carries a marker suffix, we watch
31
+ // the clean path)
32
+ this.addWatchFile(abs);
33
+ const out = hashedAssetName(abs, buf);
34
+ collected.set(out, abs);
35
+ return (
36
+ `import { __asset } from '@mosetta/runtime';\n` +
37
+ `export default __asset(${JSON.stringify('./' + out)}, import.meta.url);\n`
38
+ );
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,38 @@
1
+ import { path } from '../nodeApi.mjs';
2
+ import { CLIENT_CONDITIONS } from '../conditions.mjs';
3
+
4
+ /**
5
+ * Rollup plugin: keep `*.css` imports OUT of the module JS (System artifact carries no CSS),
6
+ * but record their resolved paths so the CSS pipeline can emit a co-located stylesheet artifact.
7
+ * Relative imports resolve against the importer; BARE imports ('@mosetta/ui/ui.css')
8
+ * resolve through the fileSystem FROM the importer's directory — library css lands in the consuming
9
+ * MODULE's scoped artifact (dependency artifacts carry no css; duplication accepted).
10
+ * `baseDir` — resolution context for the entry (it has no importer).
11
+ */
12
+ export function cssIntercept(collected, resolver, baseDir) {
13
+ return {
14
+ name: 'mosetta-css-intercept',
15
+ resolveId(source, importer) {
16
+ if (!source.endsWith('.css')) return null;
17
+ const isPathLike = source.startsWith('.') || path.isAbsolute(source);
18
+ let resolved;
19
+ if (isPathLike) {
20
+ resolved = importer
21
+ ? path.resolve(path.dirname(importer), source)
22
+ : path.resolve(source);
23
+ } else {
24
+ // resolve from the importing file's package first (pnpm: deps live per-package)
25
+ resolved = resolver.resolve(source, importer ? path.dirname(importer) : baseDir, CLIENT_CONDITIONS);
26
+ }
27
+ collected.push(resolved);
28
+ // css is read by `processCss`, not rollup (a stub is served below) — so we add it to the
29
+ // watch list here, otherwise a style edit would not invalidate the module artifact
30
+ this.addWatchFile(resolved);
31
+ return { id: resolved, external: false };
32
+ },
33
+ load(id) {
34
+ if (id.endsWith('.css')) return 'export default undefined;';
35
+ return null;
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,14 @@
1
+ import replace from '@rollup/plugin-replace';
2
+
3
+ // Replace `process.env.NODE_ENV` with a string literal at build time.
4
+ // Browsers have no `process`; CJS deps (react, react-dom, scheduler) branch on this, so without
5
+ // it they throw `process is not defined`. The literal also lets dead branches tree-shake out.
6
+ export function defineEnv(mode) {
7
+ const nodeEnv = mode === 'production' ? 'production' : 'development';
8
+ return replace({
9
+ preventAssignment: true,
10
+ values: {
11
+ 'process.env.NODE_ENV': JSON.stringify(nodeEnv),
12
+ },
13
+ });
14
+ }
@@ -0,0 +1,84 @@
1
+ import { builtinModules, path } from '../nodeApi.mjs';
2
+
3
+ /**
4
+ * Monolith compatibility with workerd — the FRAMEWORK DEFAULTS of packing: everything that used
5
+ * to be a "recipe" in the platform's mosetta.build.mjs lives here in one place and works for any
6
+ * server facet. None of it requires node — the shims are generated as virtual modules.
7
+ *
8
+ * What and why:
9
+ * - BARE standard-library names (`fs`, `process`, …): the CJS ecosystem demands them via
10
+ * DYNAMIC require too — external would yield "Dynamic require is not supported". We
11
+ * substitute a virtual shim that STATICALLY re-exports `node:*` (while `node:*` stays
12
+ * external — workerd provides it under nodejs_compat, the runner enables the flags at
13
+ * composition);
14
+ * - `cjs-module-lexer`: its `import` entry compiles wasm at runtime (forbidden in workerd).
15
+ * We resolve WITHOUT conditions (require semantics) — the package's exports map itself
16
+ * yields the pure-JS entry `lexer.js` via the `default` branch. This picks an entry the
17
+ * author declared, it is not a patch;
18
+ * - `graceful-fs`: monkey-patches fs at top level, and workerd's fs is frozen — we substitute
19
+ * the fs shim (nobody calls this package's file functions inside the isolate);
20
+ * - `@rollup/browser`: fetches its wasm via `fetch(new URL(..., import.meta.url))` and cannot
21
+ * accept a precompiled WebAssembly.Module (esbuild-wasm can: `initialize({wasmModule})`).
22
+ * The only real patch: the default URL is replaced with `globalThis.__ROLLUP_WASM__`, where
23
+ * the action puts the DECLARED wasm module. Goes away once upstream adds a parameter.
24
+ */
25
+
26
+ const SHIM_NAMESPACE = 'mosetta-node-shim';
27
+ // there is nothing esbuild can turn a `require()` of an external module into ("Dynamic require
28
+ // is not supported"), so `node:*` require calls also go to the shim — it gives a STATIC binding;
29
+ // `node:*` import declarations stay external (workerd provides them)
30
+ const REQUIRE_KINDS = new Set(['require-call', 'require-resolve']);
31
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
32
+ const BARE_BUILTINS_RE = new RegExp(`^(${builtinModules.map(escape).join('|')})$`);
33
+ const ROLLUP_WASM_URL_RE = /void 0===(\w+)&&\(\1=new URL\("bindings_wasm_bg\.wasm",[^)]*\)\)/;
34
+
35
+ export function monolithCompat(fileSystem, resolver) {
36
+ return {
37
+ name: 'mosetta-monolith-compat',
38
+ setup(build) {
39
+ build.onResolve({ filter: BARE_BUILTINS_RE }, (args) => ({
40
+ path: args.path,
41
+ namespace: SHIM_NAMESPACE,
42
+ }));
43
+ build.onResolve({ filter: /^node:/ }, (args) => (REQUIRE_KINDS.has(args.kind)
44
+ ? { path: args.path.slice('node:'.length), namespace: SHIM_NAMESPACE }
45
+ : null));
46
+ build.onResolve({ filter: /^graceful-fs$/ }, () => ({ path: 'fs', namespace: SHIM_NAMESPACE }));
47
+ build.onLoad({ filter: /.*/, namespace: SHIM_NAMESPACE }, (args) => ({
48
+ contents: `import * as ns from 'node:${args.path}';\n`
49
+ + 'export default ns.default ?? ns;\n'
50
+ + `export * from 'node:${args.path}';\n`,
51
+ loader: 'js',
52
+ }));
53
+
54
+ build.onResolve({ filter: /^cjs-module-lexer$/ }, (args) => ({
55
+ path: resolver.resolve('cjs-module-lexer', args.resolveDir || path.dirname(args.importer), null),
56
+ }));
57
+
58
+ build.onLoad({ filter: /rollup\.browser\.js$/ }, (args) => ({
59
+ contents: fileSystem.readFile(args.path, 'utf8')
60
+ .replace(ROLLUP_WASM_URL_RE, 'void 0===$1&&($1=globalThis.__ROLLUP_WASM__)'),
61
+ loader: 'js',
62
+ }));
63
+ },
64
+ };
65
+ }
66
+
67
+ /** esbuild options complementing the plugin: standard library external, URL base and cwd pinned. */
68
+ export function monolithCompatOptions() {
69
+ return {
70
+ external: ['node:*'],
71
+ define: { 'import.meta.url': JSON.stringify('file:///bundle/module.js') },
72
+ // Small runtime divergences that must be closed before the first line of foreign code:
73
+ // - workerd's process has no cwd(), and engines/postcss call it when normalizing paths
74
+ // (in the monolith's world all paths are absolute, any root will do);
75
+ // - workerd DELIBERATELY withholds FinalizationRegistry (GC observability), while the napi
76
+ // bridges of wasm packages (napi-wasm in lightningcss-wasm) register handle disposal in
77
+ // it. A no-op means handles are not freed on GC — for a short-lived build isolate that is
78
+ // bounded memory, not a production leak.
79
+ banner: {
80
+ js: 'globalThis.process&&typeof globalThis.process.cwd!=="function"&&(globalThis.process.cwd=()=>"/");'
81
+ + 'globalThis.FinalizationRegistry||(globalThis.FinalizationRegistry=class{register(){}unregister(){}});',
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,80 @@
1
+ import { path } from '../nodeApi.mjs';
2
+ import { CLIENT_CONDITIONS } from '../conditions.mjs';
3
+ import { applyBrowserField, applyBrowserEntry, BROWSER_EMPTY } from '../browserField.mjs';
4
+ import { probeFile, isDirectoryRequest } from '../probeFile.mjs';
5
+
6
+ const isOwnedByAnotherPlugin = (id) => id.includes('\0') || id.includes('?');
7
+
8
+ /**
9
+ * Resolver and loader of the module graph ON TOP OF THE PORT.
10
+ *
11
+ * Replaces two at once: `@rollup/plugin-node-resolve` and rollup's BUILT-IN resolver — both hit
12
+ * the disk past the port, so "a builder without node" held only at the outer layer. Now all
13
+ * module-graph I/O goes through `fileSystem`, and a second port implementation gets the graph
14
+ * for free.
15
+ *
16
+ * The position in the plugin list is SIGNIFICANT and mirrors the old one: where `nodeResolve`
17
+ * stood — in its place; where there was no resolver and the built-in worked — last. Otherwise
18
+ * we would intercept what used to reach `cssIntercept`, `assets`, `commonjs` or the transpiler.
19
+ */
20
+ export function portResolve(fileSystem, resolver, baseDir) {
21
+ // Symlinks are dereferenced DELIBERATELY: rollup and node-resolve do this by default
22
+ // (`preserveSymlinks: false`), and the whole graph currently lives in realpath form — a
23
+ // workspace package is seen as `packages/react/src/...`, not as a path inside node_modules.
24
+ // Without this, ids would fork and the same file would enter the bundle twice.
25
+ const canonical = (file) => {
26
+ try {
27
+ return fileSystem.realPath(file);
28
+ } catch {
29
+ return file;
30
+ }
31
+ };
32
+
33
+ return {
34
+ name: 'mosetta-port-resolve',
35
+
36
+ resolveId(source, importer) {
37
+ if (isOwnedByAnotherPlugin(source)) return null;
38
+
39
+ const fromDir = importer && !isOwnedByAnotherPlugin(importer)
40
+ ? path.dirname(importer)
41
+ : baseDir;
42
+
43
+ if (path.isAbsolute(source) || source.startsWith('.')) {
44
+ const target = path.isAbsolute(source) ? source : path.resolve(fromDir, source);
45
+ // a trailing slash disables node's file branch — decide by the ORIGINAL specifier
46
+ const found = probeFile(fileSystem, target, { directoryOnly: isDirectoryRequest(source) });
47
+ // the owning package's legacy `browser` map also hits the package's INTERNAL paths
48
+ return found ? applyBrowserField(fileSystem, canonical(found), CLIENT_CONDITIONS) : null;
49
+ }
50
+
51
+ // bare: exports maps and conditions are policy over the package catalog — the resolver's job
52
+ try {
53
+ const resolved = canonical(resolver.resolve(source, fromDir, CLIENT_CONDITIONS));
54
+ const entry = applyBrowserEntry(fileSystem, source, resolved, CLIENT_CONDITIONS);
55
+ return applyBrowserField(fileSystem, entry, CLIENT_CONDITIONS);
56
+ } catch {
57
+ return null; // let rollup decide: an external import or an honest error
58
+ }
59
+ },
60
+
61
+ load(id) {
62
+ // `"…": false` in the browser map — the module does not exist in the browser, serve a stub
63
+ if (id === BROWSER_EMPTY) return 'export default {};\n';
64
+ if (isOwnedByAnotherPlugin(id)) return null;
65
+ try {
66
+ const code = fileSystem.readFile(id, 'utf8');
67
+ // The file was read by US, not by rollup — so WE must also put it into the watch list:
68
+ // rollup fills `bundle.watchFiles` only from its own loader, which never runs with the
69
+ // port. Without this line the list is empty, and everything resting on it silently
70
+ // breaks: mtime cache invalidation and the dev server's watching of sources OUTSIDE
71
+ // `modules/` (in the monorepo that is every workspace library). The path here is already
72
+ // dereferenced (`canonical`), so a workspace symlink points at the real file.
73
+ this.addWatchFile(id);
74
+ return code;
75
+ } catch {
76
+ return null;
77
+ }
78
+ },
79
+ };
80
+ }
@@ -0,0 +1,103 @@
1
+ import { path } from '../nodeApi.mjs';
2
+ import { probeFile, isDirectoryRequest } from '../probeFile.mjs';
3
+
4
+ const LOADER = { '.ts': 'ts', '.tsx': 'tsx', '.js': 'js', '.mjs': 'js', '.cjs': 'js', '.json': 'json' };
5
+
6
+ // INPUT sourcemaps are the last channel where the engine hit the disk BY ITSELF: on seeing
7
+ // `//# sourceMappingURL=index.js.map` in a loaded file, esbuild reads that file with its own FS
8
+ // and inlines the vendor's original sources into our map. On disk this works; in an isolate the
9
+ // engine has no FS — and the artifact map diverged between environments (for
10
+ // @pnpm/crypto.polyfill: 252 `sources` entries vs 240). So we pull the map THROUGH THE PORT and
11
+ // inline it; if it is missing, we strip the reference so the engine has nowhere to go. The
12
+ // environment stops influencing the bytes.
13
+ const MAP_URL_RE = /\n?\/\/# sourceMappingURL=([^\s'"]+)[ \t]*\n?$/;
14
+
15
+ const toBase64 = (bytes) => {
16
+ let binary = '';
17
+ for (let i = 0; i < bytes.length; i += 0x8000) {
18
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
19
+ }
20
+ return btoa(binary);
21
+ };
22
+
23
+ function inlineInputSourcemap(fileSystem, file, contents) {
24
+ const hit = MAP_URL_RE.exec(contents);
25
+ if (!hit) return contents;
26
+ const url = hit[1];
27
+ if (url.startsWith('data:')) return contents; // already inline — nowhere for the engine to go
28
+ const stripped = contents.slice(0, hit.index);
29
+ try {
30
+ const map = fileSystem.readFile(path.resolve(path.dirname(file), decodeURIComponent(url)));
31
+ const bytes = typeof map === 'string' ? new TextEncoder().encode(map) : map;
32
+ return `${stripped}\n//# sourceMappingURL=data:application/json;base64,${toBase64(bytes)}\n`;
33
+ } catch {
34
+ return stripped; // map missing (or unreachable) — drop the reference: no divergence allowed
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Resolver and loader of private monoliths ON TOP OF THE PORT (esbuild half).
40
+ *
41
+ * The client path is built by rollup, monoliths by esbuild, and that is no accident: their
42
+ * output contracts differ (System with external dependencies vs a self-contained ESM). But the
43
+ * seam is the same in both engines — `onResolve`/`onLoad`, so the technique is the same too:
44
+ * esbuild stops hitting the disk with its own resolver, all I/O goes through the port.
45
+ *
46
+ * Resolution starts from `args.resolveDir` — esbuild itself puts the importer's directory there,
47
+ * and for `stdin` the configured `resolveDir`. Our own `dirname(importer)` does not work here:
48
+ * for stdin the importer is `<stdin>`, not a path.
49
+ */
50
+
51
+ // esbuild picks the `exports` branch by the KIND of import: `import` yields the ESM branch,
52
+ // `require()` the CJS one. For a dual package these are DIFFERENT files (zod: index.js vs
53
+ // index.cjs), so the import/require axis cannot be a policy constant — it depends on each
54
+ // individual reference.
55
+ const REQUIRE_KINDS = new Set(['require-call', 'require-resolve']);
56
+
57
+ export function portResolveEsbuild(fileSystem, resolver, baseDir, conditions, watched = null) {
58
+ const conditionsFor = (kind) => (REQUIRE_KINDS.has(kind)
59
+ ? conditions.map((c) => (c === 'import' ? 'require' : c))
60
+ : conditions);
61
+
62
+ // symlinks are dereferenced — as both esbuild and rollup do by default; otherwise one
63
+ // workspace file would enter the bundle twice under different ids
64
+ const canonical = (file) => {
65
+ try {
66
+ return fileSystem.realPath(file);
67
+ } catch {
68
+ return file;
69
+ }
70
+ };
71
+ // resolution also feeds the list: some files are taken over for loading by `monolithCompat`
72
+ // (vendor patches), so we never see their `onLoad` — yet we must know them for invalidation
73
+ const seen = (file) => (watched?.add(file), file);
74
+
75
+ return {
76
+ name: 'mosetta-port-resolve-esbuild',
77
+ setup(build) {
78
+ build.onResolve({ filter: /.*/ }, (args) => {
79
+ const fromDir = args.resolveDir
80
+ || (args.importer && path.isAbsolute(args.importer) ? path.dirname(args.importer) : baseDir);
81
+ if (path.isAbsolute(args.path) || args.path.startsWith('.')) {
82
+ const found = probeFile(fileSystem, path.resolve(fromDir, args.path),
83
+ { directoryOnly: isDirectoryRequest(args.path) });
84
+ return found ? { path: seen(canonical(found)) } : null;
85
+ }
86
+ try {
87
+ return { path: seen(canonical(resolver.resolve(args.path, fromDir, conditionsFor(args.kind)))) };
88
+ } catch {
89
+ return null; // let esbuild report it itself — its message is clear
90
+ }
91
+ });
92
+
93
+ // esbuild has no `bundle.watchFiles` — only the one who reads the graph knows its
94
+ // composition, i.e. us; the list is needed outside for mtime invalidation and dev-server
95
+ // watching
96
+ build.onLoad({ filter: /.*/ }, (args) => (watched?.add(args.path), {
97
+ contents: inlineInputSourcemap(fileSystem, args.path, fileSystem.readFile(args.path, 'utf8')),
98
+ loader: LOADER[path.extname(args.path)] ?? 'js',
99
+ resolveDir: path.dirname(args.path),
100
+ }));
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,56 @@
1
+ import { path } from '../nodeApi.mjs';
2
+
3
+ const TS_RE = /\.(ts|tsx)$/;
4
+ const LOADER = { '.ts': 'ts', '.tsx': 'tsx' };
5
+
6
+ // Transpile policy belongs to the BUILD, not the engine: the engine only knows "string in, string out".
7
+ const TRANSFORM = {
8
+ jsx: 'automatic',
9
+ target: 'es2020',
10
+ tsconfigRaw: { compilerOptions: { experimentalDecorators: true } },
11
+ };
12
+
13
+ /**
14
+ * TS/TSX transpilation as an ENGINE CAPABILITY, not a binding to a native binary.
15
+ *
16
+ * This used to be `rollup-plugin-esbuild`, pulling the platform esbuild binary — the last
17
+ * engine-bound node of the client path, not portable into an isolate. Now the plugin asks the
18
+ * engine for `transform(code, opts)`; which esbuild stands behind it — native or wasm — is
19
+ * decided by whoever injected the engine. Resolution is not part of this: `portResolve`
20
+ * covers it entirely.
21
+ */
22
+ export function transformTs(engine) {
23
+ return {
24
+ name: 'mosetta-transform',
25
+ async transform(code, id) {
26
+ if (!TS_RE.test(id)) return null;
27
+ const r = await engine.transform(code, {
28
+ ...TRANSFORM,
29
+ loader: LOADER[path.extname(id)],
30
+ sourcefile: id,
31
+ sourcemap: true,
32
+ });
33
+ return { code: r.code, map: r.map || null };
34
+ },
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Minification — a post-pass over the finished chunk (prod). Mangling touches only the
40
+ * artifact's internal scope: the `System.register` name, dependency specifiers and export names
41
+ * are string literals, so cross-artifact linkage survives it unchanged.
42
+ */
43
+ export function minifyChunk(engine) {
44
+ return {
45
+ name: 'mosetta-minify',
46
+ async renderChunk(code) {
47
+ const r = await engine.transform(code, {
48
+ loader: 'js',
49
+ minify: true,
50
+ target: TRANSFORM.target,
51
+ sourcemap: true,
52
+ });
53
+ return { code: r.code, map: r.map || null };
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,65 @@
1
+ import { path } from './nodeApi.mjs';
2
+
3
+ /**
4
+ * The extension probing order is build POLICY, inherited from the former
5
+ * `@rollup/plugin-node-resolve({ extensions })` setting. This is NOT node's list:
6
+ * `require.resolve` knows only `.js`/`.json`/`.node` and would never find `.ts`.
7
+ */
8
+ export const PROBE_EXTENSIONS = ['.mjs', '.js', '.ts', '.tsx', '.json'];
9
+
10
+ // depth of the `main` → directory → `main` chain: protects against a self-pointing package.json
11
+ const MAX_MAIN_DEPTH = 8;
12
+
13
+ /**
14
+ * Was a directory requested EXPLICITLY? In node a trailing slash (and also `.` and `..`)
15
+ * completely disables the file branch: `./lib/` must resolve to the directory even when
16
+ * `lib.js` sits next to it. The check must use the ORIGINAL specifier — `path.resolve`
17
+ * strips the slash.
18
+ */
19
+ export function isDirectoryRequest(source) {
20
+ return /(?:^|\/)\.{0,2}$/.test(source) || source.endsWith('/');
21
+ }
22
+
23
+ function directoryMain(fileSystem, dir) {
24
+ try {
25
+ const pkg = fileSystem.readJson(path.join(dir, 'package.json'));
26
+ return typeof pkg?.main === 'string' && pkg.main ? pkg.main : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Resolve a path to a concrete file — the shared probing for both resolvers (rollup and
34
+ * esbuild), so the rule is ONE and directly testable.
35
+ *
36
+ * Replays node's order (`Module._findPath` → `tryExtensions` / `tryPackage`):
37
+ * LOAD_AS_FILE — the exact file, then `+extension`;
38
+ * LOAD_AS_DIRECTORY — `main` from the directory's package.json, and only then `index.*`.
39
+ *
40
+ * Every item here cost a found bug: directory-over-file priority sent `./foo` to
41
+ * `foo/index.js` past `foo.js`; skipping `main` sent a directory with its own package.json to
42
+ * `index.js` past the declared entry; ignoring the trailing slash turned `./lib/` into
43
+ * `lib.js`. All three — silently.
44
+ *
45
+ * Returns an absolute path or null.
46
+ */
47
+ export function probeFile(fileSystem, target, { directoryOnly = false, depth = 0 } = {}) {
48
+ if (!directoryOnly) {
49
+ // a directory is not a file: `exists` does not know that, so we ask separately
50
+ if (fileSystem.exists(target) && !fileSystem.isDirectory(target)) return target;
51
+
52
+ const withExtension = PROBE_EXTENSIONS.map((e) => target + e).find((f) => fileSystem.exists(f));
53
+ if (withExtension) return withExtension;
54
+ }
55
+
56
+ if (!fileSystem.isDirectory(target)) return null;
57
+
58
+ const main = depth < MAX_MAIN_DEPTH ? directoryMain(fileSystem, target) : null;
59
+ if (main) {
60
+ const viaMain = probeFile(fileSystem, path.resolve(target, main), { depth: depth + 1 });
61
+ if (viaMain) return viaMain;
62
+ }
63
+
64
+ return PROBE_EXTENSIONS.map((e) => path.join(target, `index${e}`)).find((f) => fileSystem.exists(f)) ?? null;
65
+ }