@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,97 @@
|
|
|
1
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
2
|
+
import { path } from './nodeApi.mjs';
|
|
3
|
+
import { packageDirOf } from './packageMeta.mjs';
|
|
4
|
+
import { defineEnv } from './plugins/defineEnv.mjs';
|
|
5
|
+
import { portResolve } from './plugins/portResolve.mjs';
|
|
6
|
+
import { resolveSpecifier } from './specifiers.mjs';
|
|
7
|
+
import { assertNamedRegister } from './buildConfig.mjs';
|
|
8
|
+
import { userPluginsFor } from './buildModules.mjs';
|
|
9
|
+
import { sourcemapPathTransformFor } from './sourcemap.mjs';
|
|
10
|
+
import { transformTs, minifyChunk } from './plugins/transform.mjs';
|
|
11
|
+
|
|
12
|
+
const isBareExternal = (id) =>
|
|
13
|
+
!id.startsWith('.') && !path.isAbsolute(id) && !id.startsWith('\0');
|
|
14
|
+
|
|
15
|
+
const FACADE_ID = '\0mosetta-facade';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build one external dependency (npm or framework package) into a System artifact.
|
|
19
|
+
* Internal/relative imports are bundled; cross-package bare imports stay external
|
|
20
|
+
* (→ their own artifacts, resolved by the import map).
|
|
21
|
+
* For CJS deps, `namedExports` (enumerated via Node) generates a facade so the artifact
|
|
22
|
+
* exposes real named exports (needed for cross-artifact `import { x } from 'pkg'`).
|
|
23
|
+
* Returns { full, code, imports } where imports = bare deps of this artifact.
|
|
24
|
+
*/
|
|
25
|
+
export async function buildDependency(fileSystem, { resolver, specifier, resolvedFile, full, namedExports, mode = 'development', buildConfig = null, engine, sourceBase }) {
|
|
26
|
+
// this dependency's OWN transitives resolve from ITS package — dockview finds
|
|
27
|
+
// dockview-core in its .pnpm neighbourhood, no app-level declarations
|
|
28
|
+
const baseDir = packageDirOf(fileSystem, resolvedFile) ?? path.dirname(resolvedFile);
|
|
29
|
+
// maps are always EXTERNAL (placed into the private counterpart by writeJs); in dev,
|
|
30
|
+
// heavy vendor deps (react) skip map generation entirely — nobody debugs their internals
|
|
31
|
+
const isTS = /\.tsx?$/.test(resolvedFile);
|
|
32
|
+
const sourcemap = mode === 'production' || isTS;
|
|
33
|
+
const useFacade = Array.isArray(namedExports) && namedExports.length > 0;
|
|
34
|
+
const facadeCode = useFacade
|
|
35
|
+
? `import __cjs from ${JSON.stringify(resolvedFile)};\n`
|
|
36
|
+
+ `export default __cjs;\n`
|
|
37
|
+
+ `export const { ${namedExports.join(', ')} } = __cjs;\n`
|
|
38
|
+
: null;
|
|
39
|
+
|
|
40
|
+
// the rollupOptions escape hatch transforms the composed INPUT options; user
|
|
41
|
+
// plugins land after the facade virtual entry (it must keep resolving first) and before the
|
|
42
|
+
// framework chain
|
|
43
|
+
const optionsHook = (o) => buildConfig?.rollupOptions?.(o, { target: 'dependency', mode }) ?? o;
|
|
44
|
+
const bundle = await engine.rollup(optionsHook({
|
|
45
|
+
input: useFacade ? FACADE_ID : resolvedFile,
|
|
46
|
+
external: isBareExternal,
|
|
47
|
+
plugins: [
|
|
48
|
+
{
|
|
49
|
+
name: 'mosetta-facade',
|
|
50
|
+
resolveId(id) {
|
|
51
|
+
return id === FACADE_ID ? id : null;
|
|
52
|
+
},
|
|
53
|
+
load(id) {
|
|
54
|
+
return id === FACADE_ID ? facadeCode : null;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
...userPluginsFor(buildConfig, 'dependency', mode),
|
|
58
|
+
defineEnv(mode),
|
|
59
|
+
portResolve(fileSystem, resolver, baseDir),
|
|
60
|
+
commonjs(),
|
|
61
|
+
transformTs(engine), // .js (CJS deps like react) is handled by @rollup/plugin-commonjs
|
|
62
|
+
],
|
|
63
|
+
onwarn(w, warn) {
|
|
64
|
+
if (w.code === 'UNRESOLVED_IMPORT' || w.code === 'CIRCULAR_DEPENDENCY') return;
|
|
65
|
+
warn(w);
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// rewrite cross-package externals `bare` → version-qualified full-id
|
|
70
|
+
const externalMap = new Map();
|
|
71
|
+
const paths = (id) => {
|
|
72
|
+
const f = resolveSpecifier(fileSystem, resolver, id, baseDir)?.full || id;
|
|
73
|
+
externalMap.set(id, f);
|
|
74
|
+
return f;
|
|
75
|
+
};
|
|
76
|
+
// named register (SSR epic stage 1) — same rationale as buildModules.generateSystem; production
|
|
77
|
+
// minify mangles only artifact-internal scope (linkage names are string literals)
|
|
78
|
+
const { output } = await bundle.generate({
|
|
79
|
+
format: 'system', name: full, exports: 'named', sourcemap, paths,
|
|
80
|
+
sourcemapPathTransform: sourcemapPathTransformFor(sourceBase),
|
|
81
|
+
plugins: mode === 'production' ? [minifyChunk(engine)] : [],
|
|
82
|
+
});
|
|
83
|
+
const watchFiles = bundle.watchFiles.slice();
|
|
84
|
+
await bundle.close();
|
|
85
|
+
|
|
86
|
+
const chunk = output.find((o) => o.type === 'chunk');
|
|
87
|
+
assertNamedRegister(chunk.code, full, buildConfig); // protocol invariant
|
|
88
|
+
const map = sourcemap === true && chunk.map ? chunk.map.toString() : null;
|
|
89
|
+
return {
|
|
90
|
+
full,
|
|
91
|
+
jsCode: chunk.code,
|
|
92
|
+
map,
|
|
93
|
+
imports: [...externalMap.values()], // full-ids
|
|
94
|
+
rawImports: [...externalMap.keys()], // bare specifiers (for closure traversal)
|
|
95
|
+
watchFiles,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
2
|
+
import { path } from './nodeApi.mjs';
|
|
3
|
+
import { packageDirOf } from './packageMeta.mjs';
|
|
4
|
+
import { fullTileName } from './moduleName.mjs';
|
|
5
|
+
import { cssIntercept } from './plugins/cssIntercept.mjs';
|
|
6
|
+
import { assets } from './plugins/assets.mjs';
|
|
7
|
+
import { defineEnv } from './plugins/defineEnv.mjs';
|
|
8
|
+
import { portResolve } from './plugins/portResolve.mjs';
|
|
9
|
+
import { processCss } from './css.mjs';
|
|
10
|
+
import { resolveSpecifier } from './specifiers.mjs';
|
|
11
|
+
import { assertNamedRegister } from './buildConfig.mjs';
|
|
12
|
+
import { sourcemapPathTransformFor } from './sourcemap.mjs';
|
|
13
|
+
import { transformTs, minifyChunk } from './plugins/transform.mjs';
|
|
14
|
+
import { mtimeStamp } from './artifactCache.mjs';
|
|
15
|
+
|
|
16
|
+
// Every bare specifier (npm AND framework packages) is external — they become
|
|
17
|
+
// import-map-resolved System deps. Relative/absolute imports are bundled into the module.
|
|
18
|
+
// css is NEVER external: rollup consults `external` BEFORE plugin resolveId hooks, and a bare
|
|
19
|
+
// css import ('@mosetta/ui/ui.css') must reach cssIntercept, not the dep closure.
|
|
20
|
+
const isExternal = (id) =>
|
|
21
|
+
!id.endsWith('.css') && !id.startsWith('.') && !path.isAbsolute(id) && !id.startsWith('\0');
|
|
22
|
+
|
|
23
|
+
// external maps in EVERY mode: the map is a separate artifact the build places into
|
|
24
|
+
// the store's private counterpart; dev DX comes from the dev server's SourceMap response header
|
|
25
|
+
export const sourcemapFor = () => true;
|
|
26
|
+
|
|
27
|
+
// user hooks: plugins from mosetta.build.mjs land BEFORE the framework ones (React
|
|
28
|
+
// Compiler wants original JSX, an scss plugin must run before cssIntercept), the rollupOptions
|
|
29
|
+
// escape hatch transforms the composed INPUT options (output shape is protocol — not exposed)
|
|
30
|
+
export function userPluginsFor(buildConfig, target, mode) {
|
|
31
|
+
return buildConfig?.clientPlugins?.({ target, mode }) ?? [];
|
|
32
|
+
}
|
|
33
|
+
function applyOptionsHook(buildConfig, options, target, mode) {
|
|
34
|
+
return buildConfig?.rollupOptions?.(options, { target, mode }) ?? options;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// production minify is a render-chunk post-pass — mangling touches only artifact-internal scope:
|
|
38
|
+
// the System.register name, dep specifiers and export names are string literals, so the
|
|
39
|
+
// cross-artifact runtime linkage survives untouched
|
|
40
|
+
async function generateSystem({ engine, sourceBase, input, name, plugins, sourcemap = 'inline', resolveExternal, minified = false, buildConfig = null, target = 'module', mode = 'development' }) {
|
|
41
|
+
const bundle = await engine.rollup(applyOptionsHook(buildConfig, {
|
|
42
|
+
input,
|
|
43
|
+
external: isExternal,
|
|
44
|
+
plugins,
|
|
45
|
+
onwarn(w, warn) {
|
|
46
|
+
if (w.code === 'UNRESOLVED_IMPORT' || w.code === 'CIRCULAR_DEPENDENCY') return;
|
|
47
|
+
warn(w);
|
|
48
|
+
},
|
|
49
|
+
}, target, mode));
|
|
50
|
+
// output.paths rewrites every emitted external `bare` → version-qualified full-id,
|
|
51
|
+
// and records the (bare → full) of externals actually rendered into this artifact.
|
|
52
|
+
const externalMap = new Map();
|
|
53
|
+
const paths = (id) => {
|
|
54
|
+
const full = (resolveExternal && resolveExternal(id)) || id;
|
|
55
|
+
externalMap.set(id, full);
|
|
56
|
+
return full;
|
|
57
|
+
};
|
|
58
|
+
// NAMED register (SSR epic stage 1): `System.register('<full-id>', …)` — the artifact is
|
|
59
|
+
// self-identified, executable by the server mini-System AND concatenable into bundles later.
|
|
60
|
+
// The client keeps loading by URL: the named-register extra links the URL load to the name
|
|
61
|
+
// (one instance), so the browser path needs no changes.
|
|
62
|
+
const { output } = await bundle.generate({
|
|
63
|
+
format: 'system', name, exports: 'named', sourcemap, paths,
|
|
64
|
+
sourcemapPathTransform: sourcemapPathTransformFor(sourceBase),
|
|
65
|
+
plugins: minified ? [minifyChunk(engine)] : [],
|
|
66
|
+
});
|
|
67
|
+
const watchFiles = bundle.watchFiles.slice();
|
|
68
|
+
await bundle.close();
|
|
69
|
+
const chunk = output.find((o) => o.type === 'chunk');
|
|
70
|
+
assertNamedRegister(chunk.code, name, buildConfig); // protocol invariant
|
|
71
|
+
// sourcemap:'inline' embeds the map in chunk.code; true → external map returned separately
|
|
72
|
+
const map = sourcemap === true && chunk.map ? chunk.map.toString() : null;
|
|
73
|
+
return {
|
|
74
|
+
code: chunk.code,
|
|
75
|
+
map,
|
|
76
|
+
imports: [...externalMap.values()], // full-ids (the artifact's resolved dependencies)
|
|
77
|
+
rawImports: [...externalMap.keys()], // bare specifiers (for closure traversal)
|
|
78
|
+
watchFiles,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build one module to a System artifact. Returns code + descriptor (no disk write).
|
|
84
|
+
*/
|
|
85
|
+
export async function buildModule(fileSystem, mod, { resolver, mode = 'development', buildConfig = null, engine, sourceBase, assetExtensions = [] } = {}) {
|
|
86
|
+
const full = fullTileName(mod);
|
|
87
|
+
// externals resolve from the MODULE's own package — the full-id rewritten into the
|
|
88
|
+
// artifact must match what the closure walk resolves and builds from the same context
|
|
89
|
+
const baseDir = path.dirname(mod.file);
|
|
90
|
+
const cssRefs = [];
|
|
91
|
+
const jsAssets = new Map(); // out -> srcAbs (assets imported from JS)
|
|
92
|
+
const { code, map, imports, rawImports, watchFiles } = await generateSystem({
|
|
93
|
+
engine,
|
|
94
|
+
sourceBase,
|
|
95
|
+
input: mod.file,
|
|
96
|
+
name: full,
|
|
97
|
+
plugins: [
|
|
98
|
+
...userPluginsFor(buildConfig, 'module', mode),
|
|
99
|
+
defineEnv(mode), assets(jsAssets, assetExtensions, fileSystem), cssIntercept(cssRefs, resolver, baseDir), transformTs(engine),
|
|
100
|
+
// LAST: rollup's built-in resolver used to work here, and it runs after all plugins —
|
|
101
|
+
// placing it earlier would intercept css, assets and the transpiler
|
|
102
|
+
portResolve(fileSystem, resolver, baseDir),
|
|
103
|
+
],
|
|
104
|
+
sourcemap: sourcemapFor(mode),
|
|
105
|
+
resolveExternal: (id) => resolveSpecifier(fileSystem, resolver, id, baseDir)?.full,
|
|
106
|
+
minified: mode === 'production',
|
|
107
|
+
buildConfig,
|
|
108
|
+
target: 'module',
|
|
109
|
+
mode,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// css partition: a file from the module's own package is authored styles (a scoped per-module
|
|
113
|
+
// artifact, as before); a file from a FOREIGN package is dependency css and leaves as one
|
|
114
|
+
// shared artifact per closure (emitted by build()). realpath — because of workspace symlinks.
|
|
115
|
+
const ownPkg = packageDirOf(fileSystem, fileSystem.realPath(mod.file));
|
|
116
|
+
const ownCss = [];
|
|
117
|
+
const sharedCss = []; // {file, specifier: '<pkg>/<rel>', version}
|
|
118
|
+
for (const ref of cssRefs) {
|
|
119
|
+
const real = fileSystem.realPath(ref);
|
|
120
|
+
const pkgDir = packageDirOf(fileSystem, real);
|
|
121
|
+
if (!pkgDir || pkgDir === ownPkg) {
|
|
122
|
+
ownCss.push(ref);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const pkg = fileSystem.readJson(path.join(pkgDir, 'package.json'));
|
|
126
|
+
sharedCss.push({
|
|
127
|
+
file: real,
|
|
128
|
+
specifier: `${pkg.name}/${path.relative(pkgDir, real).split(path.sep).join('/')}`,
|
|
129
|
+
version: pkg.version ?? '0.0.0',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let css = null;
|
|
134
|
+
const allAssets = new Map(jsAssets);
|
|
135
|
+
if (ownCss.length) {
|
|
136
|
+
const r = await processCss({ fileSystem, cssFiles: ownCss, scopeClass: full, engine, mode });
|
|
137
|
+
css = r.css;
|
|
138
|
+
for (const [out, src] of r.assets) allAssets.set(out, src);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// entity descriptor: id + reference + assets (css is injected, the rest is preload) +
|
|
142
|
+
// graph edges; css dependencies are appended by build() (known only after the partition)
|
|
143
|
+
const descriptor = {
|
|
144
|
+
id: full,
|
|
145
|
+
reference: {
|
|
146
|
+
namespace: mod.namespace,
|
|
147
|
+
name: mod.name,
|
|
148
|
+
type: mod.type,
|
|
149
|
+
version: mod.version,
|
|
150
|
+
},
|
|
151
|
+
assets: [...(css ? [`${full}.css`] : []), ...allAssets.keys()],
|
|
152
|
+
dependencies: imports, // version-qualified full-ids (declared intent)
|
|
153
|
+
// A PAGE ROOT is a component facet only by file type: it takes the page's tree as a prop,
|
|
154
|
+
// it is what `page.reference` names, and it can never be someone's child — so the editor
|
|
155
|
+
// must not offer it as a block to place. Read off the import the facet actually made:
|
|
156
|
+
// the binding is emitted as a property access on the imported namespace, which survives
|
|
157
|
+
// minification (property names are not renamed), and merely importing the definer IS the
|
|
158
|
+
// declaration. Absent rather than false — descriptors stay free of noise.
|
|
159
|
+
...(ROOT_DEFINER_RE.test(code) ? { isRoot: true } : {}),
|
|
160
|
+
// an INTERNAL block (a tool's own — an editor panel, the editor's fallback): placed by the
|
|
161
|
+
// platform's pages, never offered to an author. The same reading, off the marker the facet
|
|
162
|
+
// imported — a wrapper over any definer, so a fallback is marked the way a component is
|
|
163
|
+
...(INTERNAL_MARK_RE.test(code) ? { internal: true } : {}),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return { full, descriptor, jsCode: code, map, css, sharedCss, assets: allAssets, watchFiles, rawImports };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** how `import { defineRootComponentSide } from '@mosetta/react'` looks once bundled */
|
|
170
|
+
const ROOT_DEFINER_RE = /\.defineRootComponentSide\b/;
|
|
171
|
+
/** …and `markInternalTile(...)` — a block the picker leaves out */
|
|
172
|
+
const INTERNAL_MARK_RE = /\.markInternalTile\b/;
|
|
173
|
+
|
|
174
|
+
const VIRTUAL_PROLOG = '\0mosetta-loader-prolog-entry';
|
|
175
|
+
|
|
176
|
+
// identical across modules, roots and rebuilds — built once per mode, but VALIDATED by the
|
|
177
|
+
// mtimes of its own source graph: the dev server process is long-lived, and a checkout (or
|
|
178
|
+
// framework-source edit in this repo) must not keep serving a stale prolog until restart
|
|
179
|
+
let prologCache = null; // { mode, jsCode, watchFiles, stamp }
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The loader PROLOG: the idempotent resolver installation
|
|
183
|
+
* (`installResolverFromPage()`) bundled as a self-contained IIFE. build() concatenates it after
|
|
184
|
+
* `systemjs + named-register` into the `system.js` co-located with every script facet — the
|
|
185
|
+
* prolog runs before any `systemjs-module` entry is processed, so the resolve hook is always
|
|
186
|
+
* installed first.
|
|
187
|
+
*/
|
|
188
|
+
export async function buildLoaderProlog(fileSystem, { resolver, mode = 'development', buildConfig = null, engine } = {}) {
|
|
189
|
+
if (prologCache && prologCache.mode === mode
|
|
190
|
+
&& prologCache.configStamp === (buildConfig?.stamp ?? 'none')
|
|
191
|
+
&& mtimeStamp(fileSystem, prologCache.watchFiles) === prologCache.stamp) return prologCache;
|
|
192
|
+
// livereload is BUILD knowledge: the dev prolog itself listens to the provider's SSE
|
|
193
|
+
// (the dev provider's /__mosetta__/reload endpoint, base — from the envelope), prod is clean
|
|
194
|
+
// by construction; the cross-cutting machinery (runner/env/shells) knows nothing about dev
|
|
195
|
+
// mode. The guard — one subscription per realm (as many prologs as script facets).
|
|
196
|
+
// The page's dev companions: SSE reload and the ERROR OVERLAY. The overlay lives here rather
|
|
197
|
+
// than as a distributive module precisely because it is needed when the build is BROKEN: there
|
|
198
|
+
// are no new artifacts, yet the prolog arrived with the last successful build and keeps
|
|
199
|
+
// working. The status sits next to the artifact tree and survives a build failure.
|
|
200
|
+
// ONE connection per ORIGIN, not per realm. A browser gives an origin about six sockets and
|
|
201
|
+
// SHARES them across tabs; an EventSource holds one of them for its whole life. The editor
|
|
202
|
+
// alone is three realms (the page plus a preview frame per canvas), so two open tabs used to
|
|
203
|
+
// pin six permanent subscriptions and the origin ran dry — a sibling frame's document could
|
|
204
|
+
// then never load (a gray canvas, an eternal spinner). So: whoever wins the web lock talks to
|
|
205
|
+
// the server and fans the news out over a BroadcastChannel; every other realm only listens.
|
|
206
|
+
const livereloadSnippet = mode === 'development'
|
|
207
|
+
? 'try {\n'
|
|
208
|
+
+ ' if (!globalThis.__mosettaReload) {\n'
|
|
209
|
+
+ ' globalThis.__mosettaReload = true;\n'
|
|
210
|
+
+ ' var show = function (status) {\n'
|
|
211
|
+
+ " var id = 'mosetta-dev-overlay';\n"
|
|
212
|
+
+ ' var node = document.getElementById(id);\n'
|
|
213
|
+
+ ' var broken = status && status.ok === false;\n'
|
|
214
|
+
+ ' if (!broken) { if (node) node.remove(); return; }\n'
|
|
215
|
+
+ ' if (globalThis.__mosettaOverlayHidden === status.stamp) return;\n'
|
|
216
|
+
+ " if (!node) { node = document.createElement('div'); node.id = id; document.body.appendChild(node); }\n"
|
|
217
|
+
+ " node.setAttribute('style', 'position:fixed;inset:0;z-index:2147483647;overflow:auto;"
|
|
218
|
+
+ "background:rgba(20,20,24,.96);color:#f5f5f5;font:12px/1.5 ui-monospace,monospace;padding:24px');\n"
|
|
219
|
+
+ " var parts = [];\n"
|
|
220
|
+
+ " if (status.build) parts.push('<b style=\"color:#ff8080\">Build failed</b>\\n\\n' + esc(status.build));\n"
|
|
221
|
+
+ " if (status.typescript) parts.push('<b style=\"color:#ffd280\">Type errors</b>\\n\\n' + esc(status.typescript));\n"
|
|
222
|
+
+ " node.innerHTML = '<button style=\"position:sticky;top:0;float:right;cursor:pointer\">close</button>"
|
|
223
|
+
+ "<pre style=\"white-space:pre-wrap;margin:0\">' + parts.join('\\n\\n') + '</pre>';\n"
|
|
224
|
+
+ " node.querySelector('button').onclick = function () {\n"
|
|
225
|
+
+ ' globalThis.__mosettaOverlayHidden = status.stamp;\n'
|
|
226
|
+
+ ' node.remove();\n'
|
|
227
|
+
+ ' };\n'
|
|
228
|
+
+ ' };\n'
|
|
229
|
+
+ " var esc = function (s) { return String(s).replace(/[&<>]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>' })[c]; }); };\n"
|
|
230
|
+
// the fan-out: news reaches every realm of the origin over the channel, so only the realm
|
|
231
|
+
// holding the lock ever opens a socket. A late-arriving realm asks for the last status
|
|
232
|
+
// instead of fetching it (`ask`), and the leader answers off its own memory
|
|
233
|
+
+ " var chan = typeof BroadcastChannel === 'function' ? new BroadcastChannel('mosetta-dev') : null;\n"
|
|
234
|
+
+ ' var leading = false;\n'
|
|
235
|
+
+ ' var last = null;\n'
|
|
236
|
+
+ ' var apply = function (msg) {\n'
|
|
237
|
+
+ ' if (msg.reload) { location.reload(); return; }\n'
|
|
238
|
+
+ ' show(msg.status);\n'
|
|
239
|
+
+ ' };\n'
|
|
240
|
+
+ ' var tell = function (msg) {\n'
|
|
241
|
+
+ ' if (chan) chan.postMessage({ mosetta: true, reload: msg.reload, status: msg.status });\n'
|
|
242
|
+
+ ' apply(msg);\n'
|
|
243
|
+
+ ' };\n'
|
|
244
|
+
+ ' if (chan) chan.onmessage = function (e) {\n'
|
|
245
|
+
+ ' var msg = e.data;\n'
|
|
246
|
+
+ ' if (!msg || !msg.mosetta) return;\n'
|
|
247
|
+
+ ' if (msg.ask) {\n'
|
|
248
|
+
+ ' if (leading && last) chan.postMessage({ mosetta: true, status: last });\n'
|
|
249
|
+
+ ' return;\n'
|
|
250
|
+
+ ' }\n'
|
|
251
|
+
+ ' apply(msg);\n'
|
|
252
|
+
+ ' };\n'
|
|
253
|
+
// ONE stream, no polling: the build status rides the same SSE (`event: status`, sent on
|
|
254
|
+
// connect and then on every change), so the browser never spends a second socket — and
|
|
255
|
+
// never learns about a broken build a whole second late either
|
|
256
|
+
+ ' var lead = function () {\n'
|
|
257
|
+
+ ' leading = true;\n'
|
|
258
|
+
+ " var src = new EventSource(new URL('__mosetta__/reload', base).href);\n"
|
|
259
|
+
+ ' src.onmessage = function (e) {\n'
|
|
260
|
+
+ " if (e.data === 'reload') tell({ reload: true });\n"
|
|
261
|
+
+ ' };\n'
|
|
262
|
+
+ " src.addEventListener('status', function (e) {\n"
|
|
263
|
+
+ ' var status = null;\n'
|
|
264
|
+
+ ' try { status = JSON.parse(e.data); } catch (err) { return; }\n'
|
|
265
|
+
+ ' last = status;\n'
|
|
266
|
+
+ ' tell({ status: status });\n'
|
|
267
|
+
+ ' });\n'
|
|
268
|
+
+ ' };\n'
|
|
269
|
+
// the lock is held for the life of the realm and released by the browser when the realm
|
|
270
|
+
// dies, so a closed (or reloaded) leader hands the connection to the next realm by itself.
|
|
271
|
+
// Without a channel there is nobody to fan out to, and without locks nobody to elect —
|
|
272
|
+
// then every realm speaks for itself, as it did before
|
|
273
|
+
+ " var locks = typeof navigator === 'object' && navigator ? navigator.locks : null;\n"
|
|
274
|
+
+ ' if (chan && locks && typeof locks.request === "function") {\n'
|
|
275
|
+
+ " locks.request('mosetta-dev-channel', function () {\n"
|
|
276
|
+
+ ' lead();\n'
|
|
277
|
+
+ ' return new Promise(function () {});\n'
|
|
278
|
+
+ ' }).catch(function () { if (!leading) lead(); });\n'
|
|
279
|
+
+ ' chan.postMessage({ mosetta: true, ask: true });\n'
|
|
280
|
+
+ ' } else {\n'
|
|
281
|
+
+ ' lead();\n'
|
|
282
|
+
+ ' }\n'
|
|
283
|
+
+ ' }\n'
|
|
284
|
+
+ '} catch (e) { /* dev-only, optional */ }\n'
|
|
285
|
+
: '';
|
|
286
|
+
const bootstrapEntry = resolver.resolveFramework('@mosetta/runtime/bootstrap');
|
|
287
|
+
const source =
|
|
288
|
+
`import { installResolverFromPage } from ${JSON.stringify(bootstrapEntry)};\n`
|
|
289
|
+
+ `const { base } = installResolverFromPage();\n${livereloadSnippet}`;
|
|
290
|
+
const bundle = await engine.rollup({
|
|
291
|
+
input: VIRTUAL_PROLOG,
|
|
292
|
+
plugins: [
|
|
293
|
+
{
|
|
294
|
+
name: 'mosetta-loader-prolog',
|
|
295
|
+
resolveId: (id) => (id === VIRTUAL_PROLOG ? id : null),
|
|
296
|
+
load: (id) => (id === VIRTUAL_PROLOG ? source : null),
|
|
297
|
+
},
|
|
298
|
+
...userPluginsFor(buildConfig, 'prolog', mode),
|
|
299
|
+
defineEnv(mode),
|
|
300
|
+
portResolve(fileSystem, resolver, path.dirname(bootstrapEntry)),
|
|
301
|
+
commonjs(),
|
|
302
|
+
transformTs(engine),
|
|
303
|
+
],
|
|
304
|
+
onwarn(w, warn) {
|
|
305
|
+
if (w.code === 'CIRCULAR_DEPENDENCY') return;
|
|
306
|
+
warn(w);
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
const { output } = await bundle.generate({
|
|
310
|
+
format: 'iife', name: 'mosettaLoaderProlog', plugins: mode === 'production' ? [minifyChunk(engine)] : [],
|
|
311
|
+
});
|
|
312
|
+
const watchFiles = bundle.watchFiles.slice();
|
|
313
|
+
// the GENERATOR is part of the prolog's graph: the livereload/overlay snippet is authored
|
|
314
|
+
// right here as a string, invisible to rollup — without this line an edit to it would keep
|
|
315
|
+
// serving stale cached prologs (in-memory AND on-disk) until the cache is wiped by hand.
|
|
316
|
+
// Non-file realms (an in-browser build) skip it: a missing path stamps as a constant.
|
|
317
|
+
if (import.meta.url.startsWith('file:')) watchFiles.push(new URL(import.meta.url).pathname);
|
|
318
|
+
await bundle.close();
|
|
319
|
+
prologCache = {
|
|
320
|
+
mode,
|
|
321
|
+
jsCode: output.find((o) => o.type === 'chunk').code,
|
|
322
|
+
watchFiles,
|
|
323
|
+
stamp: mtimeStamp(fileSystem, watchFiles),
|
|
324
|
+
configStamp: buildConfig?.stamp ?? 'none',
|
|
325
|
+
};
|
|
326
|
+
return prologCache;
|
|
327
|
+
}
|
|
328
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { path } from './nodeApi.mjs';
|
|
2
|
+
import { fullTileName } from './moduleName.mjs';
|
|
3
|
+
import { portResolveEsbuild } from './plugins/portResolveEsbuild.mjs';
|
|
4
|
+
import { monolithCompat, monolithCompatOptions } from './plugins/monolithCompat.mjs';
|
|
5
|
+
import { MONOLITH_CONDITIONS } from './conditions.mjs';
|
|
6
|
+
import { mtimeStamp } from './artifactCache.mjs';
|
|
7
|
+
|
|
8
|
+
// Private monoliths are built by esbuild (self-contained ESM, everything inside), but it no
|
|
9
|
+
// longer resolves BY ITSELF — all I/O goes through the port via `portResolveEsbuild`, and the
|
|
10
|
+
// engine itself is injected from outside (`engine.bundle`). `absWorkingDir` is pinned to the
|
|
11
|
+
// distributive root: esbuild bakes the path into module identity (`__commonJS` keys, comments),
|
|
12
|
+
// and without pinning the shape depends on the working directory — engines diverge and the
|
|
13
|
+
// build stops being reproducible.
|
|
14
|
+
// `frameworkDir` — the directory GENERATED facets resolve their imports from (the default
|
|
15
|
+
// the generated ssr default is built from a string source, which imports framework packages,
|
|
16
|
+
// `@mosetta/server` and react). It used to be derived from `import.meta.url` — and that broke
|
|
17
|
+
// exactly like the `fileSystem`/`engine` defaults did: the binding came from the CODE's location
|
|
18
|
+
// instead of being supplied from outside, and did not survive bundling into an isolate.
|
|
19
|
+
const portOptions = (fileSystem, resolver, sourceBase, frameworkDir, wasmAssets = null, watched = null) => ({
|
|
20
|
+
plugins: [
|
|
21
|
+
...(wasmAssets ? [wasmAssetsPlugin(resolver, frameworkDir, wasmAssets)] : []),
|
|
22
|
+
// workerd compatibility — BEFORE the port resolver: package substitutions and onLoad patches
|
|
23
|
+
// must fire before the port resolves/reads the files itself
|
|
24
|
+
monolithCompat(fileSystem, resolver),
|
|
25
|
+
portResolveEsbuild(fileSystem, resolver, frameworkDir, MONOLITH_CONDITIONS, watched),
|
|
26
|
+
],
|
|
27
|
+
...monolithCompatOptions(),
|
|
28
|
+
...(sourceBase ? { absWorkingDir: sourceBase } : {}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Wasm assets of a server facet: a `*.wasm` import is NOT bundled — runtime wasm compilation is
|
|
32
|
+
// forbidden in workerd, an engine can only exist as a declared bundle module. The import
|
|
33
|
+
// resolves to a file, the file travels as an asset next to the monolith (and into
|
|
34
|
+
// descriptor.assets — the runner puts it into the bundle as `{ wasm }`), while the code keeps
|
|
35
|
+
// `./<name>.wasm` — exactly the specifier the runner will declare the module under. Placed
|
|
36
|
+
// FIRST in the chain: otherwise the port resolver would resolve .wasm to a path and esbuild
|
|
37
|
+
// would try to read it as js.
|
|
38
|
+
function wasmAssetsPlugin(resolver, frameworkDir, collected) {
|
|
39
|
+
return {
|
|
40
|
+
name: 'mosetta-wasm-assets',
|
|
41
|
+
setup(build) {
|
|
42
|
+
build.onResolve({ filter: /\.wasm$/ }, (args) => {
|
|
43
|
+
const fromDir = args.resolveDir || frameworkDir;
|
|
44
|
+
const file = resolver.resolve(args.path, fromDir, MONOLITH_CONDITIONS);
|
|
45
|
+
const name = file.slice(file.lastIndexOf('/') + 1);
|
|
46
|
+
const known = collected.get(name);
|
|
47
|
+
if (known && known !== file) {
|
|
48
|
+
throw new Error(`wasm assets with the same name: ${name} (${known} and ${file})`);
|
|
49
|
+
}
|
|
50
|
+
collected.set(name, file);
|
|
51
|
+
return { path: `./${name}`, external: true };
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// server artifacts get EXTERNAL maps in every mode: they live in a private store, so
|
|
58
|
+
// the map sits right next to them (writeJs appends the co-located sourceMappingURL). esbuild with
|
|
59
|
+
// write:false emits two virtual files — pick them apart. `outdir` only shapes the virtual paths.
|
|
60
|
+
const SOURCEMAP = { sourcemap: 'external', outdir: '.' };
|
|
61
|
+
const pickOutputs = (result) => ({
|
|
62
|
+
jsCode: result.outputFiles.find((f) => !f.path.endsWith('.map')).text,
|
|
63
|
+
map: result.outputFiles.find((f) => f.path.endsWith('.map'))?.text ?? null,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Build a server-action module into a SELF-CONTAINED monolith. Unlike client modules
|
|
67
|
+
// (Rollup → SystemJS, deps externalized for singleton sharing), a server module runs alone in a Worker
|
|
68
|
+
// Loader isolate — no sharing concern — so we bundle EVERYTHING in (`external: []`) into one ESM. Web
|
|
69
|
+
// API only (`platform: 'neutral'`) → runs unchanged in the workerd isolate. The coder authors
|
|
70
|
+
// `export default defineServerSide({ services })`; the monolith re-exports `module`,
|
|
71
|
+
// which the static Worker-Loader glue imports.
|
|
72
|
+
// user hook: transforms the composed esbuild options of a server-side monolith build
|
|
73
|
+
function applyServerHook(buildConfig, options, target, mode) {
|
|
74
|
+
return buildConfig?.serverEsbuild?.(options, { target, mode }) ?? options;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function buildServerModule(fileSystem, { file, name, type, namespace, version }, { resolver, mode = 'development', buildConfig = null, engine, sourceBase, frameworkDir } = {}) {
|
|
78
|
+
const full = fullTileName({ namespace, type, name, version });
|
|
79
|
+
const wasmAssets = new Map(); // asset name -> absolute source file
|
|
80
|
+
const watched = new Set(); // the monolith's source graph — for invalidation and watching
|
|
81
|
+
const result = await engine.bundle(applyServerHook(buildConfig, {
|
|
82
|
+
...portOptions(fileSystem, resolver, sourceBase, frameworkDir, wasmAssets, watched),
|
|
83
|
+
entryPoints: [file],
|
|
84
|
+
bundle: true,
|
|
85
|
+
format: 'esm',
|
|
86
|
+
platform: 'neutral',
|
|
87
|
+
target: 'es2022',
|
|
88
|
+
write: false,
|
|
89
|
+
minify: mode === 'production',
|
|
90
|
+
logLevel: 'silent', // the caller reports a concise warn-skip on failure; suppress esbuild's own dump
|
|
91
|
+
...SOURCEMAP,
|
|
92
|
+
tsconfigRaw: { compilerOptions: { experimentalDecorators: true, jsx: 'automatic' } },
|
|
93
|
+
}, 'server', mode));
|
|
94
|
+
return { full, wasmAssets, watchFiles: [...watched], ...pickOutputs(result) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- ssr facet of app modules (the shell is a degenerate SSR case) --------------------------
|
|
98
|
+
|
|
99
|
+
// The DEFAULT ssr grain: generated when the app module has no <name>.ssr.tsx of its own
|
|
100
|
+
// (convention-with-override). A coder's grain replaces it entirely — that
|
|
101
|
+
// is also where another binding plugs in: the definer lives WITH its binding (@mosetta/react/ssr
|
|
102
|
+
// over the server's engine), so a preact page writes preact's.
|
|
103
|
+
//
|
|
104
|
+
// It resolves from the APP's directory, not the framework's, precisely because of that import:
|
|
105
|
+
// which binding renders a page is the APP's choice, and the app is what has it installed.
|
|
106
|
+
const DEFAULT_SSR_SOURCE = `import { defineSsrSide } from '@mosetta/react/ssr';
|
|
107
|
+
|
|
108
|
+
export default defineSsrSide();
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
// the default bundles react-dom/server — identical across app modules and rebuilds, so memoize
|
|
112
|
+
// (keyed by the build-config stamp too: a serverEsbuild hook edit must not serve a stale default).
|
|
113
|
+
// VALIDATED by its graph's mtime, like the prolog: the dev server lives long, and the default
|
|
114
|
+
// facet is built from the FRAMEWORK's sources — in the monorepo they are hand-edited and must
|
|
115
|
+
// be picked up.
|
|
116
|
+
let defaultSsrCache = null; // { mode, configStamp, jsCode, map, watchFiles, stamp }
|
|
117
|
+
|
|
118
|
+
export async function buildSsrModule(fileSystem, { file, name, namespace, version }, { resolver, mode = 'development', buildConfig = null, engine, sourceBase, frameworkDir } = {}) {
|
|
119
|
+
const full = fullTileName({ namespace, type: 'ssr', name, version });
|
|
120
|
+
const grain = path.join(path.dirname(file), `${name}.ssr.tsx`);
|
|
121
|
+
const custom = fileSystem.exists(grain);
|
|
122
|
+
|
|
123
|
+
const configStamp = buildConfig?.stamp ?? 'none';
|
|
124
|
+
if (!custom && defaultSsrCache && defaultSsrCache.mode === mode && defaultSsrCache.configStamp === configStamp
|
|
125
|
+
&& mtimeStamp(fileSystem, defaultSsrCache.watchFiles) === defaultSsrCache.stamp) {
|
|
126
|
+
return {
|
|
127
|
+
full, custom, jsCode: defaultSsrCache.jsCode, map: defaultSsrCache.map,
|
|
128
|
+
watchFiles: defaultSsrCache.watchFiles, watchFile: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const watched = new Set();
|
|
132
|
+
const result = await engine.bundle(applyServerHook(buildConfig, {
|
|
133
|
+
...portOptions(fileSystem, resolver, sourceBase, frameworkDir, null, watched),
|
|
134
|
+
...(custom
|
|
135
|
+
? { entryPoints: [grain] }
|
|
136
|
+
: { stdin: { contents: DEFAULT_SSR_SOURCE, resolveDir: sourceBase ?? frameworkDir, sourcefile: `${name}.ssr.default.ts`, loader: 'ts' } }),
|
|
137
|
+
bundle: true,
|
|
138
|
+
format: 'esm',
|
|
139
|
+
platform: 'neutral',
|
|
140
|
+
target: 'es2022',
|
|
141
|
+
write: false,
|
|
142
|
+
minify: mode === 'production',
|
|
143
|
+
logLevel: 'silent',
|
|
144
|
+
...SOURCEMAP,
|
|
145
|
+
// react packages branch on it; workerd has no `process`
|
|
146
|
+
define: { ...monolithCompatOptions().define, 'process.env.NODE_ENV': JSON.stringify(mode) },
|
|
147
|
+
tsconfigRaw: { compilerOptions: { jsx: 'automatic' } },
|
|
148
|
+
}, 'ssr', mode));
|
|
149
|
+
const { jsCode, map } = pickOutputs(result);
|
|
150
|
+
const watchFiles = [...watched];
|
|
151
|
+
if (!custom) {
|
|
152
|
+
defaultSsrCache = { mode, configStamp, jsCode, map, watchFiles, stamp: mtimeStamp(fileSystem, watchFiles) };
|
|
153
|
+
}
|
|
154
|
+
return { full, custom, jsCode, map, watchFiles, watchFile: custom ? grain : null };
|
|
155
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { path } from './nodeApi.mjs';
|
|
2
|
+
import { parse, init } from 'cjs-module-lexer';
|
|
3
|
+
|
|
4
|
+
// the lexer's wasm is embedded in the package as base64 (no fetch, no file) — init is one-shot and lazy
|
|
5
|
+
let ready = null;
|
|
6
|
+
function ensureLexer() {
|
|
7
|
+
if (!ready) ready = init();
|
|
8
|
+
return ready;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
12
|
+
// `default` — arrives separately via the facade; `__esModule` is an interop MARKER, not an
|
|
13
|
+
// export: it is set non-enumerable (`Object.defineProperty`), so the old runtime walk never saw it.
|
|
14
|
+
const NOT_AN_EXPORT = new Set(['default', '__esModule']);
|
|
15
|
+
const LEXABLE_EXT = new Set(['.js', '.cjs']);
|
|
16
|
+
const MAX_REEXPORT_DEPTH = 16;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Named exports of a CJS module — by STATIC parsing.
|
|
20
|
+
*
|
|
21
|
+
* Needed by the dependency artifact's facade: a cross-artifact
|
|
22
|
+
* `import { createContext } from 'react'` requires the artifact to serve REAL named exports —
|
|
23
|
+
* default-based CJS interop loses them at artifact boundaries.
|
|
24
|
+
*
|
|
25
|
+
* The list used to be obtained by executing the module (`require(file)` + `Object.keys`).
|
|
26
|
+
* Parsing the string instead of executing removes three things at once: running foreign code on
|
|
27
|
+
* the build machine, depending on the node loader, and the last reason to keep `cjsNamedExports`
|
|
28
|
+
* in the port — the function only needs `readFile` and `resolve`, so it lives OUTSIDE the port
|
|
29
|
+
* and works with any implementation of it. The lexer is the same `cjs-module-lexer` node itself
|
|
30
|
+
* uses to determine CJS named exports.
|
|
31
|
+
*
|
|
32
|
+
* Returns string[], or null when this is not parseable CJS (ESM, another extension, a syntax
|
|
33
|
+
* error) — then rollup handles the module natively, as before.
|
|
34
|
+
*/
|
|
35
|
+
export async function cjsNamedExports(fileSystem, resolver, file, fromDir) {
|
|
36
|
+
if (!LEXABLE_EXT.has(path.extname(file))) return null;
|
|
37
|
+
await ensureLexer();
|
|
38
|
+
|
|
39
|
+
const collected = new Set();
|
|
40
|
+
const visited = new Set();
|
|
41
|
+
|
|
42
|
+
// `module.exports = require('./cjs/react.production.js')` — the form half of real-world npm
|
|
43
|
+
// lives in: the entry itself exports NOTHING, everything is in the re-export target.
|
|
44
|
+
const walk = (target, dir, depth) => {
|
|
45
|
+
if (depth > MAX_REEXPORT_DEPTH || visited.has(target)) return true;
|
|
46
|
+
visited.add(target);
|
|
47
|
+
|
|
48
|
+
let source;
|
|
49
|
+
try {
|
|
50
|
+
source = fileSystem.readFile(target, 'utf8');
|
|
51
|
+
} catch {
|
|
52
|
+
return false; // file missing / unreadable — treat the parse as failed
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = parse(source, target);
|
|
58
|
+
} catch {
|
|
59
|
+
return false; // ESM syntax or broken CJS — let rollup decide on its own
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const name of parsed.exports) {
|
|
63
|
+
if (!NOT_AN_EXPORT.has(name) && IDENTIFIER.test(name)) collected.add(name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nextDir = path.dirname(target);
|
|
67
|
+
for (const specifier of parsed.reexports) {
|
|
68
|
+
let resolved;
|
|
69
|
+
try {
|
|
70
|
+
resolved = resolver.resolve(specifier, nextDir);
|
|
71
|
+
} catch {
|
|
72
|
+
continue; // unresolvable re-export (conditional/optional) — simply adds no names
|
|
73
|
+
}
|
|
74
|
+
if (!LEXABLE_EXT.has(path.extname(resolved))) continue;
|
|
75
|
+
if (!walk(resolved, nextDir, depth + 1)) return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return walk(file, fromDir, 0) ? [...collected] : null;
|
|
81
|
+
}
|