@mosetta/build 0.0.0-stage → 0.0.10
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 +160 -0
- package/LICENSE +21 -0
- package/README.md +14 -2
- package/package.json +36 -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/node.mjs +113 -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
package/src/build.mjs
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { crypto, path } from './nodeApi.mjs';
|
|
2
|
+
import { packageDirOf } from './packageMeta.mjs';
|
|
3
|
+
import { discoverRoots, discoverModulesIn, discoverServerModulesIn, discoverRouterModulesIn, discoverPrefabsIn, discoverPagesIn, readConfig } from './discovery.mjs';
|
|
4
|
+
import { buildModule, buildLoaderProlog } from './buildModules.mjs';
|
|
5
|
+
import { fullTileName } from './moduleName.mjs';
|
|
6
|
+
import { buildServerModule, buildSsrModule } from './buildServer.mjs';
|
|
7
|
+
import { buildDependency } from './buildDependencies.mjs';
|
|
8
|
+
import { resolveSpecifier, dependencyFullId } from './specifiers.mjs';
|
|
9
|
+
import { cjsNamedExports } from './cjsExports.mjs';
|
|
10
|
+
import { processCss } from './css.mjs';
|
|
11
|
+
import { INERT_BUILD_CONFIG } from './buildConfig.mjs';
|
|
12
|
+
import { formatBytes } from './format.mjs';
|
|
13
|
+
import { mtimeStamp } from './artifactCache.mjs';
|
|
14
|
+
import { describeSettings, SETTINGS_SUFFIX } from './settings.mjs';
|
|
15
|
+
|
|
16
|
+
// entry barrel `.`: exposes the build itself and the size formatter for the host's report
|
|
17
|
+
export { formatBytes };
|
|
18
|
+
export { INERT_BUILD_CONFIG } from './buildConfig.mjs';
|
|
19
|
+
|
|
20
|
+
// core + the named-register extra: artifacts are NAMED System.register('<full-id>', …) bundles
|
|
21
|
+
// (SSR epic stage 1) — the extra links a URL load to its name (one instance both ways).
|
|
22
|
+
// Read THROUGH THE PORT and lazily: importing the build module must not touch the disk by itself.
|
|
23
|
+
let systemjsCache = null;
|
|
24
|
+
function readSystemjs(fileSystem, resolver) {
|
|
25
|
+
if (!systemjsCache) {
|
|
26
|
+
systemjsCache = `${fileSystem.readFile(resolver.resolveFramework('systemjs/dist/system.js'), 'utf8')}\n${
|
|
27
|
+
fileSystem.readFile(resolver.resolveFramework('systemjs/dist/extras/named-register.js'), 'utf8')}`;
|
|
28
|
+
}
|
|
29
|
+
return systemjsCache;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Distributive layout: the in-memory FS (and any disk mirror) is split into two top-level
|
|
33
|
+
// namespaces so nothing leaks across the trust boundary on deploy:
|
|
34
|
+
// public/ — client-facing static (modules + co-located loaders, deps, prefabs, pages, manifests).
|
|
35
|
+
// Its CONTENTS are what a CDN serves at its root; the dev provider serves it at `/`.
|
|
36
|
+
// private/ — server-only artifacts (server-action monoliths, later server info). Never public;
|
|
37
|
+
// the render worker reaches it via the private channel (dev `/private`, prod S3).
|
|
38
|
+
// The internal structure inside `public/` is unchanged — only the top-level folder is new.
|
|
39
|
+
const PUB = 'public';
|
|
40
|
+
|
|
41
|
+
// Snapshot of the artifact tree — IN MEMORY ONLY. Disk mirroring is deliberately ABSENT here:
|
|
42
|
+
// the build writes nothing, the snapshot is returned by value, and what to do with it is the
|
|
43
|
+
// caller's decision. This is not tidiness but a property: the port stays read-only, and the
|
|
44
|
+
// build isolate has no write capability at all.
|
|
45
|
+
class OutputFS {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.files = new Map(); // relPath -> content
|
|
48
|
+
}
|
|
49
|
+
set(rel, content) {
|
|
50
|
+
this.files.set(rel, content);
|
|
51
|
+
}
|
|
52
|
+
get(rel) {
|
|
53
|
+
return this.files.get(rel);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Full build of a distributive into an in-memory artifact-tree snapshot.
|
|
59
|
+
* Returns { fs, prefabs, pages, watchFiles, watchDirs, report }.
|
|
60
|
+
*
|
|
61
|
+
* The function READS through the port and mutates nothing: no writes, no execution
|
|
62
|
+
* (`mosetta.build.mjs` arrives already loaded — `buildConfig`; the blocks' settings schemas are run
|
|
63
|
+
* by `evaluate`, the host's, when it gives one). `fileSystem`, `engine`, `log`, `frameworkDir`
|
|
64
|
+
* have no defaults on purpose: the environment binding is supplied from outside, otherwise a
|
|
65
|
+
* default would quietly pull node back into portable code.
|
|
66
|
+
*
|
|
67
|
+
* `released` — the ids already in the catalog the build goes into (a platform builds into its
|
|
68
|
+
* own): a library root whose distribution is there and a published package that is there are not
|
|
69
|
+
* built again. Absent — everything is built, as a local build does.
|
|
70
|
+
*/
|
|
71
|
+
export async function build(root, { mode = 'development', log, fileSystem, resolver, engine, frameworkDir, buildConfig = INERT_BUILD_CONFIG, cache = null, evaluate = null, released = null }) {
|
|
72
|
+
const out = new OutputFS();
|
|
73
|
+
const watchFiles = new Set();
|
|
74
|
+
const addWatch = (arr) => arr.forEach((f) => watchFiles.add(f));
|
|
75
|
+
// The cross-iteration artifact cache (`artifactCache.mjs`) is an ENVIRONMENT concern, like the
|
|
76
|
+
// port and the engine: the long-lived dev server passes one, a one-shot prod build passes
|
|
77
|
+
// nothing and builds everything.
|
|
78
|
+
const memo = cache
|
|
79
|
+
? (key, produce, filesOf) => cache.memo(key, produce, filesOf)
|
|
80
|
+
: (key, produce) => produce();
|
|
81
|
+
|
|
82
|
+
// Sourcemap rule: a JS artifact's map lives in the PRIVATE counterpart of its store
|
|
83
|
+
// at the same relative path (public/ → private/, content-public/ → content-private/); the public
|
|
84
|
+
// js carries NO sourceMappingURL — the pairing is convention, the dev server surfaces it via the
|
|
85
|
+
// SourceMap response header. Artifacts already in a private zone keep the map NEXT to them, with
|
|
86
|
+
// the co-located comment (nothing leaks — the whole zone is behind the trust boundary).
|
|
87
|
+
const MAP_ZONES = [['public/', 'private/'], ['content-public/', 'content-private/']];
|
|
88
|
+
// the bundlers stamp their own chunk-named comment — normalize it away, writeJs owns the linkage
|
|
89
|
+
const stripMapComment = (code) => code.replace(/\n?\/\/# sourceMappingURL=\S*\s*$/, '\n');
|
|
90
|
+
const writeJs = (rel, code, map) => {
|
|
91
|
+
if (!map) {
|
|
92
|
+
out.set(rel, code);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const zone = MAP_ZONES.find(([pub]) => rel.startsWith(pub));
|
|
96
|
+
if (zone) {
|
|
97
|
+
out.set(rel, stripMapComment(code));
|
|
98
|
+
out.set(`${zone[1]}${rel.slice(zone[0].length)}.map`, map);
|
|
99
|
+
} else {
|
|
100
|
+
out.set(rel, `${stripMapComment(code)}//# sourceMappingURL=${path.basename(rel)}.map\n`);
|
|
101
|
+
out.set(`${rel}.map`, map);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// SRI hash of the artifact's SERVED bytes: computed after emission (writeJs strips the map
|
|
106
|
+
// comment for the public zone — we hash exactly what goes over the wire)
|
|
107
|
+
const sriOf = (rel) => `sha256-${crypto.createHash('sha256').update(out.get(rel)).digest('base64')}`;
|
|
108
|
+
|
|
109
|
+
// 1) distributive roots (app + libraries; devLibraries only outside production) → modules
|
|
110
|
+
const roots = discoverRoots(fileSystem, root, { resolver, includeDev: mode !== 'production', log });
|
|
111
|
+
// ALREADY RELEASED where this build goes (`released` — the ids of the target's catalog): a library
|
|
112
|
+
// whose distribution is there at this version is there whole — its blocks, facets, server modules
|
|
113
|
+
// and prefabs — and is not built again. The app itself always builds.
|
|
114
|
+
const releasedRoot = (r) => r.dir !== root && !!released?.has(fullTileName({ namespace: r.namespace, type: 'distribution', name: r.namespace, version: r.version }));
|
|
115
|
+
const builtRoots = roots.filter((r) => !releasedRoot(r));
|
|
116
|
+
const mods = builtRoots.flatMap((r) => discoverModulesIn(fileSystem, r));
|
|
117
|
+
log(`mosetta: ${roots.length} root(s)${builtRoots.length < roots.length ? ` (${roots.length - builtRoots.length} released — not built)` : ''}, ${mods.length} client tile side(s)`);
|
|
118
|
+
// composition inputs are watched too: mosetta.json (library list), package.json (version feeds
|
|
119
|
+
// full-ids) of the app and every root, plus the app's content config.json (languages, namespace) —
|
|
120
|
+
// a checkout or a hand edit that changes ANY of them must trigger a rescan-rebuild, not require
|
|
121
|
+
// a dev-server restart (chokidar accepts not-yet-existing paths and fires on creation)
|
|
122
|
+
for (const dir of new Set([root, ...roots.map((r) => r.dir)])) {
|
|
123
|
+
watchFiles.add(path.join(dir, 'mosetta.json'));
|
|
124
|
+
watchFiles.add(path.join(dir, 'package.json'));
|
|
125
|
+
}
|
|
126
|
+
watchFiles.add(path.join(root, 'config.json'));
|
|
127
|
+
|
|
128
|
+
// build-extension hooks: mosetta.build.mjs of the APP root, fresh per build() run
|
|
129
|
+
// (mtime cache-bust) — an edit of the config is a composition change like any other
|
|
130
|
+
if (buildConfig.file) watchFiles.add(buildConfig.file);
|
|
131
|
+
// the data-level knob rides along: extra asset extensions (a custom asset type in
|
|
132
|
+
// the simple case needs no plugin at all)
|
|
133
|
+
const assetExtensions = readConfig(fileSystem, root).assetExtensions ?? [];
|
|
134
|
+
|
|
135
|
+
// Artifact-cache EPOCH: everything that changes the build as a whole rather than through
|
|
136
|
+
// individual graph files — mode, config hooks, the package catalog (a reinstall changes
|
|
137
|
+
// resolution without touching sources) and the composition inputs (mosetta.json/package.json of
|
|
138
|
+
// the roots, config.json, mosetta.build.mjs itself). An epoch change wipes the cache entirely;
|
|
139
|
+
// within an epoch artifacts live by the mtime of their graphs.
|
|
140
|
+
cache?.epoch([mode, buildConfig?.stamp ?? 'none', resolver.stamp, mtimeStamp(fileSystem, [...watchFiles])].join('|'));
|
|
141
|
+
|
|
142
|
+
// the loader for script facets: systemjs + named-register + resolver prolog,
|
|
143
|
+
// co-located into EVERY script facet's folder — a System.register entry is not executable
|
|
144
|
+
// without the loader, and the module folder gives it a version-qualified (immutable) URL.
|
|
145
|
+
let loaderJs = null;
|
|
146
|
+
if (mods.some((m) => m.type === 'script')) {
|
|
147
|
+
const prolog = await buildLoaderProlog(fileSystem, { resolver, mode, buildConfig, engine, sourceBase: root });
|
|
148
|
+
addWatch(prolog.watchFiles);
|
|
149
|
+
loaderJs = `${readSystemjs(fileSystem, resolver)}\n${prolog.jsCode}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// catalog of LOGICAL artifacts for the report (renderBuildReport): distributive → module → facets
|
|
153
|
+
const catalog = new Map(); // ns -> { version, modules: Map(name -> [{facet, bytes, info}]) }
|
|
154
|
+
const record = ({ namespace, version, name }, facet, bytes, info = '') => {
|
|
155
|
+
const dist = catalog.get(namespace) ?? { version, modules: new Map() };
|
|
156
|
+
catalog.set(namespace, dist);
|
|
157
|
+
const facets = dist.modules.get(name) ?? [];
|
|
158
|
+
dist.modules.set(name, facets);
|
|
159
|
+
facets.push({ facet, bytes, info });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// dependency css: one shared UNscoped artifact per closure (identity — the dependency-artifact
|
|
163
|
+
// formula); delivery goes through the catalog graph
|
|
164
|
+
const sharedCssArtifacts = new Map(); // cssFull -> {file, specifier, version}
|
|
165
|
+
const entityDescriptors = []; // descriptors of PACKAGE entities (deps + css) for the aggregate
|
|
166
|
+
|
|
167
|
+
const moduleResults = [];
|
|
168
|
+
let skippedSettings = 0;
|
|
169
|
+
for (const m of mods) {
|
|
170
|
+
const cached = await memo(`module:${fullTileName(m)}`,
|
|
171
|
+
() => buildModule(fileSystem, m, { resolver, mode, buildConfig, engine, sourceBase: root, assetExtensions }));
|
|
172
|
+
// the descriptor below gets AUGMENTED (edges to css entities, integrity) — the cached object
|
|
173
|
+
// must stay untouched, otherwise edits would accumulate from iteration to iteration
|
|
174
|
+
const r = { ...cached, descriptor: { ...cached.descriptor, dependencies: [...cached.descriptor.dependencies] } };
|
|
175
|
+
moduleResults.push(r);
|
|
176
|
+
addWatch(r.watchFiles);
|
|
177
|
+
for (const s of r.sharedCss) {
|
|
178
|
+
const cssFull = dependencyFullId(s.specifier, s.version);
|
|
179
|
+
if (!sharedCssArtifacts.has(cssFull)) sharedCssArtifacts.set(cssFull, s);
|
|
180
|
+
// a css dependency is a graph node: an edge in the importer's descriptor
|
|
181
|
+
if (!r.descriptor.dependencies.includes(cssFull)) r.descriptor.dependencies.push(cssFull);
|
|
182
|
+
}
|
|
183
|
+
writeJs(`${PUB}/modules/${r.full}/${r.full}.js`, r.jsCode, r.map);
|
|
184
|
+
r.descriptor.integrity = sriOf(`${PUB}/modules/${r.full}/${r.full}.js`);
|
|
185
|
+
// a block's ICON: `<name>.icon.svg` beside its module — shipped in the module's folder under a
|
|
186
|
+
// name of its content (a new icon is a new name: no cache keeps the old one) and named by the
|
|
187
|
+
// descriptor; the pickers show it
|
|
188
|
+
if (m.type === 'component') {
|
|
189
|
+
const iconFile = path.join(path.dirname(m.file), `${m.name}.icon.svg`);
|
|
190
|
+
watchFiles.add(iconFile);
|
|
191
|
+
if (fileSystem.exists(iconFile)) {
|
|
192
|
+
const icon = fileSystem.readFile(iconFile);
|
|
193
|
+
r.descriptor.icon = `${r.full}.icon.${contentHash(icon)}.svg`;
|
|
194
|
+
out.set(`${PUB}/modules/${r.full}/${r.descriptor.icon}`, icon);
|
|
195
|
+
}
|
|
196
|
+
// its SETTINGS: `<name>.settings.ts` beside it — what the block's node keeps, as JSON Schema
|
|
197
|
+
// in the descriptor (settings.mjs); run by the host, skipped where the host runs nothing
|
|
198
|
+
const settingsFile = path.join(path.dirname(m.file), `${m.name}${SETTINGS_SUFFIX}`);
|
|
199
|
+
watchFiles.add(settingsFile);
|
|
200
|
+
if (fileSystem.exists(settingsFile)) {
|
|
201
|
+
if (evaluate) {
|
|
202
|
+
const described = await memo(`settings:${r.full}`,
|
|
203
|
+
() => describeSettings(fileSystem, settingsFile, { resolver, engine, evaluate, frameworkDir, sourceBase: root }));
|
|
204
|
+
addWatch(described.watchFiles);
|
|
205
|
+
r.descriptor.settings = described.schema;
|
|
206
|
+
} else {
|
|
207
|
+
skippedSettings++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
out.set(`${PUB}/modules/${r.full}/${r.full}.json`, JSON.stringify(r.descriptor, null, 2));
|
|
212
|
+
if (r.css) out.set(`${PUB}/modules/${r.full}/${r.full}.css`, r.css);
|
|
213
|
+
if (m.type === 'script') out.set(`${PUB}/modules/${r.full}/system.js`, loaderJs);
|
|
214
|
+
let assetBytes = 0;
|
|
215
|
+
for (const [outName, srcAbs] of r.assets) {
|
|
216
|
+
const content = fileSystem.readFile(srcAbs);
|
|
217
|
+
out.set(`${PUB}/modules/${r.full}/${outName}`, content);
|
|
218
|
+
assetBytes += content.length;
|
|
219
|
+
}
|
|
220
|
+
record(m, m.type, r.jsCode.length, r.assets.size ? `${r.assets.size} asset(s) ${formatBytes(assetBytes)}` : '');
|
|
221
|
+
// css gets its own row right under its facet (and honestly counts into the distributive total)
|
|
222
|
+
if (r.css) record(m, 'css', r.css.length);
|
|
223
|
+
}
|
|
224
|
+
if (skippedSettings) log(`mosetta: ${skippedSettings} settings schema(s) not read — this environment runs no code at build time`);
|
|
225
|
+
|
|
226
|
+
// 2) — the ROOT-LEVEL runtime files (bootstrap.js/system.js and their content-public copies)
|
|
227
|
+
// are GONE: the loader ships co-located with script facets (§ above), the
|
|
228
|
+
// bootstrap logic lives in @mosetta/runtime shipped as an ordinary dependency artifact.
|
|
229
|
+
|
|
230
|
+
// 3) dependency closure — keyed by version-qualified full-id. Specifiers resolve
|
|
231
|
+
// FROM THE IMPORTER (node semantics): a module's externals from its package, a
|
|
232
|
+
// dependency's transitives from its own .pnpm neighbourhood — the app manifest no longer
|
|
233
|
+
// carries foreign transitives (scheduler/dockview/zod-style leaks). Dedup builds by full-id,
|
|
234
|
+
// so distinct versions of one lib produce distinct artifacts. No import map is emitted: the
|
|
235
|
+
// runtime resolver derives each URL by formula (modules/<full-id>/<full-id>.js — flat layout).
|
|
236
|
+
const seen = new Set(); // full-ids already built
|
|
237
|
+
const queued = new Set(); // importerDir|specifier pairs already enqueued
|
|
238
|
+
const queue = []; // [specifier, importerDir]
|
|
239
|
+
const enqueue = (specs, importerDir) => specs.forEach((s) => {
|
|
240
|
+
const key = `${importerDir}|${s}`;
|
|
241
|
+
if (!queued.has(key)) {
|
|
242
|
+
queued.add(key);
|
|
243
|
+
queue.push([s, importerDir]);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
for (let i = 0; i < mods.length; i++) enqueue(moduleResults[i].rawImports, path.dirname(mods[i].file));
|
|
247
|
+
|
|
248
|
+
let depCount = 0;
|
|
249
|
+
let releasedDeps = 0;
|
|
250
|
+
const depRows = []; // {specifier, version, bytes} — for the dependency-artifact report
|
|
251
|
+
while (queue.length) {
|
|
252
|
+
const [specifier, importerDir] = queue.shift();
|
|
253
|
+
const resolved = resolveSpecifier(fileSystem, resolver, specifier, importerDir, log);
|
|
254
|
+
if (!resolved) continue;
|
|
255
|
+
const { full, resolvedFile, kind, version } = resolved;
|
|
256
|
+
if (seen.has(full)) continue;
|
|
257
|
+
seen.add(full);
|
|
258
|
+
// a PUBLISHED package already released where this build goes is there with everything it
|
|
259
|
+
// imports (released together with it): neither it nor its closure is built again. A package of
|
|
260
|
+
// the distribution's own sources always is — new bytes under its version must be seen
|
|
261
|
+
if (released?.has(full) && fileSystem.realPath(resolvedFile).split(/[\\/]/).includes('node_modules')) {
|
|
262
|
+
releasedDeps++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// a dependency artifact is cached together with its cjs-export scan: both read the same
|
|
267
|
+
// package files and only change together with them
|
|
268
|
+
const res = await memo(`dep:${full}`, async () => {
|
|
269
|
+
const namedExports = kind === 'builtin' ? null : await cjsNamedExports(fileSystem, resolver, resolvedFile, importerDir);
|
|
270
|
+
return buildDependency(fileSystem, { resolver, specifier, resolvedFile, full, namedExports, mode, buildConfig, engine, sourceBase: root });
|
|
271
|
+
});
|
|
272
|
+
addWatch(res.watchFiles);
|
|
273
|
+
writeJs(`${PUB}/modules/${full}/${full}.js`, res.jsCode, res.map);
|
|
274
|
+
const depVersion = version;
|
|
275
|
+
const depDescriptor = {
|
|
276
|
+
id: full,
|
|
277
|
+
reference: { type: 'package', specifier, version: depVersion },
|
|
278
|
+
assets: [],
|
|
279
|
+
dependencies: res.imports,
|
|
280
|
+
integrity: sriOf(`${PUB}/modules/${full}/${full}.js`),
|
|
281
|
+
// built from the distribution's OWN sources (a workspace package), not a published one: new
|
|
282
|
+
// bytes under its version are a change that needs a new version — a published package's
|
|
283
|
+
// bytes may differ by build tooling alone, and its first deployed copy stands (deploy.mjs)
|
|
284
|
+
...(fileSystem.realPath(resolvedFile).split(/[\\/]/).includes('node_modules') ? {} : { local: true }),
|
|
285
|
+
};
|
|
286
|
+
out.set(`${PUB}/modules/${full}/${full}.json`, JSON.stringify(depDescriptor, null, 2));
|
|
287
|
+
entityDescriptors.push(depDescriptor);
|
|
288
|
+
depRows.push({ specifier, version: depVersion, bytes: res.jsCode.length });
|
|
289
|
+
// this artifact's own imports resolve from ITS package next
|
|
290
|
+
enqueue(res.rawImports, packageDirOf(fileSystem, resolvedFile) ?? path.dirname(resolvedFile));
|
|
291
|
+
depCount++;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 3b) css entities: a shared UNscoped css artifact is a full catalog entity
|
|
295
|
+
// (folder: css + url() assets + descriptor, no JS facet)
|
|
296
|
+
for (const [cssFull, s] of sharedCssArtifacts) {
|
|
297
|
+
if (released?.has(cssFull)) continue; // released with the package it comes from
|
|
298
|
+
// a dependency's css entity is the file itself plus its url() assets; there is no graph here,
|
|
299
|
+
// so we assemble the watched set ourselves (otherwise editing an image would not change its
|
|
300
|
+
// hash-based name)
|
|
301
|
+
const { css, assets: cssAssets } = await memo(`css:${cssFull}`,
|
|
302
|
+
async () => {
|
|
303
|
+
const r = await processCss({ fileSystem, cssFiles: [s.file], scopeClass: null, engine, mode });
|
|
304
|
+
return { ...r, watchFiles: [s.file, ...r.assets.values()] };
|
|
305
|
+
});
|
|
306
|
+
out.set(`${PUB}/modules/${cssFull}/${cssFull}.css`, css);
|
|
307
|
+
for (const [outName, srcAbs] of cssAssets) {
|
|
308
|
+
out.set(`${PUB}/modules/${cssFull}/${outName}`, fileSystem.readFile(srcAbs));
|
|
309
|
+
}
|
|
310
|
+
const cssDescriptor = {
|
|
311
|
+
id: cssFull,
|
|
312
|
+
// type 'css' (not 'package'): the entity has no JS facet — preloader/linking tell these apart
|
|
313
|
+
reference: { type: 'css', specifier: s.specifier, version: s.version },
|
|
314
|
+
assets: [`${cssFull}.css`, ...cssAssets.keys()],
|
|
315
|
+
dependencies: [],
|
|
316
|
+
integrity: sriOf(`${PUB}/modules/${cssFull}/${cssFull}.css`),
|
|
317
|
+
};
|
|
318
|
+
out.set(`${PUB}/modules/${cssFull}/${cssFull}.json`, JSON.stringify(cssDescriptor, null, 2));
|
|
319
|
+
entityDescriptors.push(cssDescriptor);
|
|
320
|
+
watchFiles.add(s.file);
|
|
321
|
+
depRows.push({ specifier: `${s.specifier} (css)`, version: s.version, bytes: css.length });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 3c) DISTRIBUTIONS name themselves: one entity per root that brings blocks or prefabs — its
|
|
325
|
+
// title, description, icon, cover and where its source lives. The descriptor is the root's
|
|
326
|
+
// mosetta.json; the description falls back to package.json's, the source to its `repository`
|
|
327
|
+
// (url + directory). Files ship in the entity's folder under names of their content.
|
|
328
|
+
for (const r of builtRoots) {
|
|
329
|
+
// the APP's own prefabs are its site's content (the picker's "prefabs of this site"), not a
|
|
330
|
+
// distribution: the app counts only for blocks of its own
|
|
331
|
+
const brings = mods.some((m) => m.namespace === r.namespace)
|
|
332
|
+
|| (r.dir !== root && discoverPrefabsIn(fileSystem, r.dir, () => {}).length > 0);
|
|
333
|
+
if (!brings) continue;
|
|
334
|
+
const descriptor = distributionDescriptor(fileSystem, r, (key, file) => {
|
|
335
|
+
watchFiles.add(file);
|
|
336
|
+
if (!fileSystem.exists(file)) {
|
|
337
|
+
log(` ! ${r.namespace}: its ${key} ${path.relative(r.dir, file)} is not there — skipped`);
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const bytes = fileSystem.readFile(file);
|
|
341
|
+
const name = `${key}.${contentHash(bytes)}${path.extname(file)}`;
|
|
342
|
+
out.set(`${PUB}/modules/${fullTileName({ namespace: r.namespace, type: 'distribution', name: r.namespace, version: r.version })}/${name}`, bytes);
|
|
343
|
+
return name;
|
|
344
|
+
});
|
|
345
|
+
out.set(`${PUB}/modules/${descriptor.id}/${descriptor.id}.json`, JSON.stringify(descriptor, null, 2));
|
|
346
|
+
entityDescriptors.push(descriptor);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 4) manifests + prefabs (namespaces.json is dead — namespaces derive from the catalog).
|
|
350
|
+
// scripts.json/styles.json/dependencies.json are dead — their roles are carried by the catalog
|
|
351
|
+
// below. The unified entity catalog: an aggregate of ALL public descriptors of the provider —
|
|
352
|
+
// modules + package entities (deps, css). Replaces scripts.json / styles.json /
|
|
353
|
+
// dependencies.json: a script facet's presence = a `_t_script_` entity in the catalog, css
|
|
354
|
+
// lives in the entity's assets, the graph is the descriptor edges. One fetch for
|
|
355
|
+
// shell/runner/editor.
|
|
356
|
+
// prefabs = reusable templates (all roots); pages = routed/servable trees (app root only — they are
|
|
357
|
+
// the application's pages, not a library's). Both stored per-file; the render path reads PAGES.
|
|
358
|
+
// a prefab is filed under its AUTHOR — the root's namespace — so two libraries may name theirs
|
|
359
|
+
// alike; a page embeds one as `_ns_<namespace>_t_prefab_n_<name>_v_live`
|
|
360
|
+
const prefabs = builtRoots.flatMap((r) => discoverPrefabsIn(fileSystem, r.dir, log).map((p) => ({ ...p, namespace: r.namespace, library: r.dir !== root, version: r.version })));
|
|
361
|
+
// a library's prefab is LIVE — taken at its current form wherever it is placed — and it names the
|
|
362
|
+
// blocks of the distributions built with it at the versions they are built at now, whatever
|
|
363
|
+
// version its file was written at
|
|
364
|
+
const builtVersions = new Map(roots.map((r) => [r.namespace, r.version]));
|
|
365
|
+
for (const p of prefabs) out.set(`${PUB}/prefabs/${p.namespace}/${p.name}.json`, JSON.stringify(atBuiltVersions(p.json, builtVersions), null, 2));
|
|
366
|
+
// …and the catalog names it, so the editor can offer it beside its distribution's blocks
|
|
367
|
+
for (const p of prefabs.filter((q) => q.library)) {
|
|
368
|
+
entityDescriptors.push({
|
|
369
|
+
id: fullTileName({ namespace: p.namespace, type: 'prefab', name: p.name, version: p.version }),
|
|
370
|
+
reference: { namespace: p.namespace, name: p.name, type: 'prefab', version: p.version },
|
|
371
|
+
assets: [],
|
|
372
|
+
dependencies: [],
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const entityCatalog = [...moduleResults.map((r) => r.descriptor), ...entityDescriptors];
|
|
377
|
+
out.set(`${PUB}/modules.json`, JSON.stringify(entityCatalog, null, 2));
|
|
378
|
+
const pages = discoverPagesIn(fileSystem, root, log);
|
|
379
|
+
for (const pg of pages) out.set(`${PUB}/pages/${pg.name}.json`, JSON.stringify(pg.json, null, 2));
|
|
380
|
+
|
|
381
|
+
// 5) server-action modules → PRIVATE monoliths: one self-contained ESM per module, run
|
|
382
|
+
// in a Worker Loader isolate; not client artifacts, not split into deps. Legacy @mosetta/* server
|
|
383
|
+
// modules can't bundle for Web API yet (node builtins) → each is warn-skipped (not silently dropped)
|
|
384
|
+
// until the framework is consolidated into @mosetta/server-action.
|
|
385
|
+
//
|
|
386
|
+
// PRIVATE CATALOG: private entities carry the same descriptors as public ones:
|
|
387
|
+
// id + reference + assets + dependencies + integrity, a per-entity json next to the monolith
|
|
388
|
+
// plus the private/modules.json aggregate. This gives the runner the isolate's file list and an
|
|
389
|
+
// integrity check on delivery, and gives future action assets (wasm build engines) a place to be
|
|
390
|
+
// declared. dependencies is always [] — a monolith is self-contained.
|
|
391
|
+
const privateDescriptors = [];
|
|
392
|
+
const recordPrivate = (full, reference, extra = {}, moreAssets = []) => {
|
|
393
|
+
const base = `private/modules/${full}`;
|
|
394
|
+
const descriptor = {
|
|
395
|
+
id: full,
|
|
396
|
+
reference,
|
|
397
|
+
assets: [...moreAssets, ...(out.get(`${base}/${full}.js.map`) ? [`${full}.js.map`] : [])],
|
|
398
|
+
dependencies: [],
|
|
399
|
+
integrity: sriOf(`${base}/${full}.js`),
|
|
400
|
+
...extra,
|
|
401
|
+
};
|
|
402
|
+
out.set(`${base}/${full}.json`, JSON.stringify(descriptor, null, 2));
|
|
403
|
+
privateDescriptors.push(descriptor);
|
|
404
|
+
};
|
|
405
|
+
const serverMods = builtRoots.flatMap((r) => discoverServerModulesIn(fileSystem, r));
|
|
406
|
+
let serverCount = 0;
|
|
407
|
+
for (const sm of serverMods) {
|
|
408
|
+
try {
|
|
409
|
+
const s = await memo(`server:${fullTileName(sm)}`,
|
|
410
|
+
() => buildServerModule(fileSystem, sm, { resolver, mode, buildConfig, engine, sourceBase: root, frameworkDir }));
|
|
411
|
+
addWatch(s.watchFiles);
|
|
412
|
+
writeJs(`private/modules/${s.full}/${s.full}.js`, s.jsCode, s.map);
|
|
413
|
+
// the facet's wasm imports travel as assets next to the monolith: the runner will declare
|
|
414
|
+
// them as bundle modules under the same relative names
|
|
415
|
+
for (const [assetName, srcFile] of s.wasmAssets) {
|
|
416
|
+
out.set(`private/modules/${s.full}/${assetName}`, fileSystem.readFile(srcFile));
|
|
417
|
+
}
|
|
418
|
+
recordPrivate(s.full, { namespace: sm.namespace, name: sm.name, type: 'server', version: sm.version },
|
|
419
|
+
{}, [...s.wasmAssets.keys()]);
|
|
420
|
+
record(sm, 'server', s.jsCode.length, 'private'
|
|
421
|
+
+ (s.wasmAssets.size ? `, ${s.wasmAssets.size} wasm asset(s)` : ''));
|
|
422
|
+
serverCount++;
|
|
423
|
+
} catch (e) {
|
|
424
|
+
log(` ! server side ${sm.name}@${sm.namespace} not bundled (${String(e.message || e)})`
|
|
425
|
+
+ ` — skipped; needs @mosetta/server-action Web-API port`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// no glue artifacts anymore: the UNIVERSAL glue is embedded in the runner and
|
|
429
|
+
// composed with every isolate — monoliths carry their aspects themselves.
|
|
430
|
+
|
|
431
|
+
// 6) there is NO router (ADR-0053): the runner dispatches by path and the site isolate routes —
|
|
432
|
+
// an authored `.router.ts` facet is a mistake to name, not a file to ignore
|
|
433
|
+
const routerFacets = roots.flatMap((r) => discoverRouterModulesIn(fileSystem, r));
|
|
434
|
+
if (routerFacets.length) {
|
|
435
|
+
throw new Error(`mosetta: router sides are no longer built — the site line routes (${routerFacets.map((rm) => path.relative(root, rm.file)).join(', ')})`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 7) ssr facet per ROOT-CAPABLE module (the shell is a degenerate SSR case): the
|
|
439
|
+
// coder's <name>.ssr.tsx or the generated react default → private/modules/<full-id>/ —
|
|
440
|
+
// what the site isolate of its engine runs. Root-capable = component facet + script facet in one module
|
|
441
|
+
// (the script facet marks root-capability, the default ssr grain is keyed off it;
|
|
442
|
+
// script-only modules are pure early scripts — nothing to render).
|
|
443
|
+
let ssrCount = 0;
|
|
444
|
+
const scriptDirs = new Set(mods.filter((m) => m.type === 'script').map((m) => path.dirname(m.file)));
|
|
445
|
+
const ssrRoots = mods.filter((m) => m.type === 'component' && scriptDirs.has(path.dirname(m.file)));
|
|
446
|
+
for (const am of ssrRoots) {
|
|
447
|
+
const s = await memo(`ssr:${fullTileName(am)}`,
|
|
448
|
+
() => buildSsrModule(fileSystem, am, { resolver, mode, buildConfig, engine, sourceBase: root, frameworkDir }));
|
|
449
|
+
if (s.watchFile) watchFiles.add(s.watchFile);
|
|
450
|
+
addWatch(s.watchFiles);
|
|
451
|
+
writeJs(`private/modules/${s.full}/${s.full}.js`, s.jsCode, s.map);
|
|
452
|
+
recordPrivate(s.full, { namespace: am.namespace, name: am.name, type: 'ssr', version: am.version });
|
|
453
|
+
record(am, 'ssr', s.jsCode.length, s.watchFile ? 'private' : 'private, generated default');
|
|
454
|
+
ssrCount++;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// the private-zone aggregate is the twin of public/modules.json: one fetch
|
|
458
|
+
// for the runner — composition, integrity, well-known pointers
|
|
459
|
+
out.set('private/modules.json', JSON.stringify(privateDescriptors, null, 2));
|
|
460
|
+
|
|
461
|
+
log(`mosetta: ${moduleResults.length} tile side(s), ${depCount} package tile(s)${releasedDeps ? ` (${releasedDeps} released — not built)` : ''}, ${prefabs.length} prefab(s), ${pages.length} page(s)`
|
|
462
|
+
+ (serverCount ? `, ${serverCount} server monolith(s)` : '')
|
|
463
|
+
+ `, ${ssrCount} ssr`);
|
|
464
|
+
const watchDirs = [
|
|
465
|
+
...roots.flatMap((r) => [path.join(r.dir, 'tiles'), path.join(r.dir, 'prefabs')]),
|
|
466
|
+
path.join(root, 'pages'),
|
|
467
|
+
];
|
|
468
|
+
const report = {
|
|
469
|
+
distributions: [...catalog].map(([namespace, d]) => ({
|
|
470
|
+
namespace,
|
|
471
|
+
version: d.version,
|
|
472
|
+
modules: [...d.modules].map(([name, facets]) => ({ name, facets })),
|
|
473
|
+
})),
|
|
474
|
+
dependencies: depRows,
|
|
475
|
+
};
|
|
476
|
+
return { fs: out, prefabs, pages, watchFiles: [...watchFiles], watchDirs, report };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** a short name of some bytes' content — what makes a shipped file's name change with it */
|
|
480
|
+
function contentHash(bytes) {
|
|
481
|
+
return crypto.createHash('sha256').update(bytes).digest('hex').slice(0, 10);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** a GitHub-like repository field (`git+https://github.com/o/r.git` + directory) as a link to the folder */
|
|
485
|
+
export function sourceLinkOf(repository) {
|
|
486
|
+
const url = typeof repository === 'string' ? repository : repository?.url;
|
|
487
|
+
if (typeof url !== 'string') return null;
|
|
488
|
+
const base = url.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^git@github\.com:/, 'https://github.com/');
|
|
489
|
+
if (!/^https?:\/\//.test(base)) return null;
|
|
490
|
+
const directory = typeof repository === 'object' && typeof repository.directory === 'string' ? repository.directory.replace(/^\/+|\/+$/g, '') : '';
|
|
491
|
+
return directory ? `${base}/tree/HEAD/${directory}` : base;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* A distribution's catalog entity: what it calls itself, from its mosetta.json (title,
|
|
496
|
+
* description, icon, cover, source) with package.json behind the description and the source.
|
|
497
|
+
* `ship(key, file)` puts a file into the entity's folder and answers its shipped name (null: none).
|
|
498
|
+
*/
|
|
499
|
+
export function distributionDescriptor(fileSystem, root, ship) {
|
|
500
|
+
const cfg = readConfig(fileSystem, root.dir);
|
|
501
|
+
let pkg = {};
|
|
502
|
+
try {
|
|
503
|
+
pkg = fileSystem.readJson(path.join(root.dir, 'package.json'));
|
|
504
|
+
} catch { /* a root without package.json names no description or source */ }
|
|
505
|
+
const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : undefined);
|
|
506
|
+
const files = {};
|
|
507
|
+
for (const key of ['icon', 'cover']) {
|
|
508
|
+
if (typeof cfg[key] !== 'string') continue;
|
|
509
|
+
const name = ship(key, path.join(root.dir, cfg[key]));
|
|
510
|
+
if (name) files[key] = name;
|
|
511
|
+
}
|
|
512
|
+
const descriptor = {
|
|
513
|
+
id: fullTileName({ namespace: root.namespace, type: 'distribution', name: root.namespace, version: root.version }),
|
|
514
|
+
reference: { namespace: root.namespace, name: root.namespace, type: 'distribution', version: root.version },
|
|
515
|
+
assets: [],
|
|
516
|
+
dependencies: [],
|
|
517
|
+
title: text(cfg.title) ?? root.namespace,
|
|
518
|
+
// a tool's own distribution (the editor): carries no blocks for authors — no card, no switch
|
|
519
|
+
...(cfg.internal === true ? { internal: true } : {}),
|
|
520
|
+
// the distribution others are built on (the UI kit): always connected, no switch
|
|
521
|
+
...(cfg.base === true ? { base: true } : {}),
|
|
522
|
+
...(text(cfg.description) ?? text(pkg.description) ? { description: text(cfg.description) ?? text(pkg.description) } : {}),
|
|
523
|
+
...(text(cfg.source) ?? sourceLinkOf(pkg.repository) ? { source: text(cfg.source) ?? sourceLinkOf(pkg.repository) } : {}),
|
|
524
|
+
...(changesOf(fileSystem, root) ? { changes: changesOf(fileSystem, root) } : {}),
|
|
525
|
+
...files,
|
|
526
|
+
};
|
|
527
|
+
return descriptor;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** a module reference with a version (not a live prefab): its namespace, the rest, its version */
|
|
531
|
+
const VERSIONED_REFERENCE_RE = /^_ns_(.+?)(_t_[a-z][A-Za-z0-9]*_n_.+)_v_(\d+(?:_\d+)*)$/;
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* A library prefab's tree with every block of a distribution built with it at that distribution's
|
|
535
|
+
* version now (`versions`: namespace → X.Y.Z) — a prefab written when its kit was 0.0.1 names the
|
|
536
|
+
* kit's blocks at 0.0.1, and is shipped live with the kit at 0.0.2. Other references stay.
|
|
537
|
+
*/
|
|
538
|
+
export function atBuiltVersions(json, versions) {
|
|
539
|
+
const visit = (node) => {
|
|
540
|
+
if (!node || typeof node !== 'object') return node;
|
|
541
|
+
const match = typeof node.reference === 'string' ? VERSIONED_REFERENCE_RE.exec(node.reference) : null;
|
|
542
|
+
const version = match ? versions.get(match[1]) : undefined;
|
|
543
|
+
const reference = version ? `_ns_${match[1]}${match[2]}_v_${version.replace(/[^a-zA-Z0-9]/g, '_')}` : node.reference;
|
|
544
|
+
return {
|
|
545
|
+
...node,
|
|
546
|
+
...(reference !== node.reference ? { reference } : {}),
|
|
547
|
+
...(Array.isArray(node.children) ? { children: node.children.map(visit) } : {}),
|
|
548
|
+
};
|
|
549
|
+
};
|
|
550
|
+
return json && typeof json === 'object' && json.root ? { ...json, root: visit(json.root) } : json;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** the most a version's notes may say in the catalog — what a window has room for */
|
|
554
|
+
const MAX_CHANGES = 4000;
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* What this version says it changed: its section of the distribution's CHANGELOG.md — the lines
|
|
558
|
+
* under a heading naming the version (`## 1.2.0`, as changesets writes it), up to the next heading
|
|
559
|
+
* of that level. Nothing for a distribution without one, or a version it does not name.
|
|
560
|
+
*/
|
|
561
|
+
export function changesOf(fileSystem, root) {
|
|
562
|
+
const file = path.join(root.dir, 'CHANGELOG.md');
|
|
563
|
+
if (!fileSystem.exists(file)) return undefined;
|
|
564
|
+
const lines = fileSystem.readFile(file, 'utf8').split(/\r?\n/);
|
|
565
|
+
const at = lines.findIndex((line) => new RegExp(`^(#{1,3})\\s*\\[?v?${root.version.replace(/\./g, '\\.')}\\]?(\\s|$)`).test(line));
|
|
566
|
+
if (at < 0) return undefined;
|
|
567
|
+
const level = /^#+/.exec(lines[at])[0].length;
|
|
568
|
+
const section = [];
|
|
569
|
+
for (const line of lines.slice(at + 1)) {
|
|
570
|
+
const heading = /^(#+)\s/.exec(line);
|
|
571
|
+
if (heading && heading[1].length <= level) break;
|
|
572
|
+
section.push(line);
|
|
573
|
+
}
|
|
574
|
+
const notes = section.join('\n').trim();
|
|
575
|
+
return notes ? notes.slice(0, MAX_CHANGES) : undefined;
|
|
576
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// mosetta.build.mjs: the app's build-extension hooks. mosetta.json stays pure data —
|
|
2
|
+
// build CODE lives in this co-located convention file. Loaded per build() run with an mtime
|
|
3
|
+
// cache-bust (the ESM import cache is permanent, a re-import needs a fresh URL), so a dev-server
|
|
4
|
+
// rebuild picks up config edits without a restart. Missing file → inert config.
|
|
5
|
+
|
|
6
|
+
/** Inert form: `mosetta.build.mjs` is absent or the caller did not load it. */
|
|
7
|
+
export const INERT_BUILD_CONFIG = Object.freeze({
|
|
8
|
+
file: null, stamp: 'none', clientPlugins: null, serverEsbuild: null, rollupOptions: null,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// The protocol invariant a user plugin must not break: a client artifact is a
|
|
13
|
+
// NAMED System.register — the runtime links artifacts by these string names. Checked after every
|
|
14
|
+
// generate; quote-agnostic (production minify normalizes quotes).
|
|
15
|
+
export function assertNamedRegister(code, full, buildConfig) {
|
|
16
|
+
const re = new RegExp(`System\\.register\\(\\s*["']${full.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']`);
|
|
17
|
+
if (re.test(code)) return;
|
|
18
|
+
const hint = buildConfig?.file && buildConfig.stamp !== 'none'
|
|
19
|
+
? ` — check your plugins in ${buildConfig.file} (reproduce without them first)`
|
|
20
|
+
: '';
|
|
21
|
+
throw new Error(`artifact ${full} lost its named System.register form${hint}`);
|
|
22
|
+
}
|