@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.
- package/CHANGELOG.md +144 -0
- package/LICENSE +21 -0
- package/README.md +14 -2
- package/package.json +34 -3
- package/src/archive.mjs +140 -0
- package/src/artifactCache.mjs +78 -0
- package/src/browserField.mjs +90 -0
- package/src/build.mjs +576 -0
- package/src/buildConfig.mjs +22 -0
- package/src/buildDependencies.mjs +97 -0
- package/src/buildModules.mjs +328 -0
- package/src/buildServer.mjs +155 -0
- package/src/cjsExports.mjs +81 -0
- package/src/conditions.mjs +21 -0
- package/src/configBundle.mjs +59 -0
- package/src/css.mjs +108 -0
- package/src/discovery.mjs +184 -0
- package/src/fileSystem.mjs +69 -0
- package/src/format.mjs +4 -0
- package/src/hash.mjs +13 -0
- package/src/memoryFileSystem.mjs +81 -0
- package/src/moduleName.mjs +11 -0
- package/src/nodeApi.mjs +22 -0
- package/src/packageMeta.mjs +37 -0
- package/src/packagesResolver.mjs +159 -0
- package/src/plugins/assets.mjs +41 -0
- package/src/plugins/cssIntercept.mjs +38 -0
- package/src/plugins/defineEnv.mjs +14 -0
- package/src/plugins/monolithCompat.mjs +84 -0
- package/src/plugins/portResolve.mjs +80 -0
- package/src/plugins/portResolveEsbuild.mjs +103 -0
- package/src/plugins/transform.mjs +56 -0
- package/src/probeFile.mjs +65 -0
- package/src/release.mjs +516 -0
- package/src/scanner.mjs +397 -0
- package/src/settings.mjs +39 -0
- package/src/sourcemap.mjs +26 -0
- package/src/specifiers.mjs +76 -0
|
@@ -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
|
+
}
|