@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.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `exports` conditions of the target environments.
3
+ *
4
+ * Conditions are a statement about the TARGET, not a property of the resolver. A dual package's
5
+ * `require` and `import` branches are different files, and if the condition is not set it gets
6
+ * picked as a side effect of what the resolver is written with: `createRequire` always says
7
+ * "I am require, I am node" about itself, even when building code for the browser. The failure
8
+ * is silent — the build passes, things break later.
9
+ *
10
+ * That is why the policy lives here, apart from the mechanics, and is visible as a whole.
11
+ */
12
+
13
+ // Client artifacts execute in the browser (SystemJS) AND in ssr isolates (MiniSystem, workerd) —
14
+ // it is the same artifact set. The browser ESM branch suits both environments: it contains no
15
+ // node specifics by construction.
16
+ export const CLIENT_CONDITIONS = ['browser', 'import'];
17
+
18
+ // A private monolith is a self-contained ESM targeting Web API (`platform: 'neutral'`). We do
19
+ // not claim `browser` here: the monolith executes in an isolate, not in a document, and a
20
+ // package's browser branch may pull DOM dependencies.
21
+ export const MONOLITH_CONDITIONS = ['import'];
@@ -0,0 +1,59 @@
1
+ import { path } from './nodeApi.mjs';
2
+ import { portResolveEsbuild } from './plugins/portResolveEsbuild.mjs';
3
+ import { monolithCompat, monolithCompatOptions } from './plugins/monolithCompat.mjs';
4
+ import { MONOLITH_CONDITIONS } from './conditions.mjs';
5
+
6
+ /**
7
+ * Bundling the tenant's `mosetta.build.mjs` FOR PHASE 2 of recomposition.
8
+ *
9
+ * The config is foreign code and is NOT executed here: only bundled into one ESM module with
10
+ * the same compatibility set as the monoliths (builtin shims, node:* external, patches). ONLY
11
+ * the runner's composition makes it executable — the module lands in the phase-2 isolate's
12
+ * bundle under the name CONFIG_MODULE, and the action imports it from the bundle.
13
+ *
14
+ * The config's closure is already in the body: the scanner collects seeds from
15
+ * mosetta.build.mjs itself too (SOURCE_EXTENSIONS includes .mjs), so its dependencies arrive
16
+ * normally. A non-portable config (node natives, unresolvable imports) fails here with a
17
+ * bundling error — it goes into the text of the loud refusal.
18
+ */
19
+
20
+ export const CONFIG_MODULE = 'mosetta.config.js';
21
+
22
+ export async function bundleBuildConfig(fileSystem, resolver, { engine, root, frameworkDir }) {
23
+ const file = path.join(root, 'mosetta.build.mjs');
24
+ const result = await engine.bundle({
25
+ entryPoints: [file],
26
+ bundle: true,
27
+ format: 'esm',
28
+ platform: 'neutral',
29
+ target: 'es2022',
30
+ write: false,
31
+ logLevel: 'silent',
32
+ absWorkingDir: root,
33
+ plugins: [
34
+ monolithCompat(fileSystem, resolver),
35
+ portResolveEsbuild(fileSystem, resolver, frameworkDir, MONOLITH_CONDITIONS),
36
+ ],
37
+ ...monolithCompatOptions(),
38
+ });
39
+ return result.outputFiles.find((f) => !f.path.endsWith('.map')).text;
40
+ }
41
+
42
+ const asFn = (v, name) => {
43
+ if (v === undefined || v === null) return null;
44
+ if (typeof v !== 'function') throw new Error(`mosetta.build.mjs: \`${name}\` must be a function`);
45
+ return v;
46
+ };
47
+
48
+ /** Hooks from an ALREADY IMPORTED (by composition) config module — same shape as the CLI's loadBuildConfig. */
49
+ export function buildConfigFrom(ns) {
50
+ const cfg = ns?.default ?? {};
51
+ if (typeof cfg !== 'object') throw new Error('mosetta.build.mjs must default-export an object of hooks');
52
+ return {
53
+ file: 'mosetta.build.mjs',
54
+ stamp: 'recomposed', // the composition's identity already includes the config module's hash
55
+ clientPlugins: asFn(cfg.clientPlugins, 'clientPlugins'),
56
+ serverEsbuild: asFn(cfg.serverEsbuild, 'serverEsbuild'),
57
+ rollupOptions: asFn(cfg.rollupOptions, 'rollupOptions'),
58
+ };
59
+ }
package/src/css.mjs ADDED
@@ -0,0 +1,108 @@
1
+ import { path } from './nodeApi.mjs';
2
+ import postcss from 'postcss';
3
+ import valueParser from 'postcss-value-parser';
4
+ import { hashedAssetName } from './hash.mjs';
5
+
6
+ // Scope every rule's selectors under `.<scopeClass>` (skip @keyframes steps).
7
+ function scopePlugin(scopeClass) {
8
+ const plugin = () => ({
9
+ postcssPlugin: 'mosetta-scope',
10
+ // Once + walkRules visits each existing rule exactly once (the Rule visitor re-runs on mutation).
11
+ Once(root) {
12
+ root.walkRules((rule) => {
13
+ const parent = rule.parent;
14
+ if (parent && parent.type === 'atrule' && /keyframes$/i.test(parent.name)) return;
15
+ rule.selectors = rule.selectors.map((s) => `.${scopeClass} ${s}`);
16
+ });
17
+ },
18
+ });
19
+ plugin.postcss = true;
20
+ return plugin();
21
+ }
22
+
23
+ // Neutralize global root selectors (html/body/:root/:host) in UNscoped shared css: under the
24
+ // per-module scope these rules were dead (`.<full> body` never matches) — we keep the same
25
+ // semantics deterministically and byte-identically: a dead-class prefix. Disciplined css
26
+ // (.dui-root) never touches this path.
27
+ function neutralizeGlobals() {
28
+ const GLOBAL_ROOT = /^(html|body|:root|:host)(?![\w-])/;
29
+ const plugin = () => ({
30
+ postcssPlugin: 'mosetta-neutralize-globals',
31
+ Once(root) {
32
+ root.walkRules((rule) => {
33
+ const parent = rule.parent;
34
+ if (parent && parent.type === 'atrule' && /keyframes$/i.test(parent.name)) return;
35
+ rule.selectors = rule.selectors.map((s) => (GLOBAL_ROOT.test(s.trim()) ? `.mosetta-neutralized-global ${s}` : s));
36
+ });
37
+ },
38
+ });
39
+ plugin.postcss = true;
40
+ return plugin();
41
+ }
42
+
43
+ // Rewrite relative url(...) refs → ./<hashed>, collecting assets to co-locate next to the stylesheet.
44
+ function urlRewrite(fileSystem, cssDir, assets) {
45
+ const plugin = () => ({
46
+ postcssPlugin: 'mosetta-url-rewrite',
47
+ Declaration(decl) {
48
+ if (!decl.value || !decl.value.includes('url(')) return;
49
+ const parsed = valueParser(decl.value);
50
+ parsed.walk((node) => {
51
+ if (node.type !== 'function' || node.value !== 'url' || !node.nodes[0]) return;
52
+ const urlNode = node.nodes[0];
53
+ const raw = urlNode.value;
54
+ if (!raw || /^(https?:|data:|#|\/)/.test(raw)) return; // skip absolute / data / anchor
55
+ const abs = path.resolve(cssDir, raw);
56
+ if (!fileSystem.exists(abs)) return;
57
+ const buf = fileSystem.readFile(abs);
58
+ const out = hashedAssetName(abs, buf);
59
+ assets.set(out, abs);
60
+ urlNode.value = './' + out;
61
+ });
62
+ decl.value = parsed.toString();
63
+ },
64
+ });
65
+ plugin.postcss = true;
66
+ return plugin();
67
+ }
68
+
69
+ /**
70
+ * Css minification — a prod post-pass by the SAME engine that minifies js (`minifyChunk`).
71
+ *
72
+ * A dedicated css tool (lightningcss and the like) is deliberately not bundled: the only thing
73
+ * we were missing is compression, and the engine already has it; a full css compiler is +16 MB
74
+ * of wasm in EVERY isolate's bundle (the whole build bundle is 18.9 MB today) for percentage
75
+ * points of compression and browserslist lowering — i.e. for one distributive's configuration.
76
+ * Whoever needs that plugs their own into `mosetta.build.mjs` (the hook path was verified with
77
+ * lightningcss-wasm exactly).
78
+ *
79
+ * `target` is NOT set: we want compression, not syntax rewriting — lowering for old browsers is
80
+ * the tenant's choice, and the build must not silently change css semantics on their behalf.
81
+ */
82
+ async function minifyCss(engine, css) {
83
+ const r = await engine.transform(css, { loader: 'css', minify: true });
84
+ return r.code;
85
+ }
86
+
87
+ /**
88
+ * Process a module's CSS files: scope selectors with `.<scopeClass>` (dev+prod) and rewrite
89
+ * relative url() to co-located hashed assets. Returns { css, assets: Map(out -> srcAbs) }.
90
+ * scopeClass === null → no scoping (dependency css isolates itself).
91
+ *
92
+ * Minification — production only and AFTER scoping/`url()` rewriting: compression gets exactly
93
+ * the css that would ship in the distributive, with scope prefixes and `./<hashed>` links in place.
94
+ */
95
+ export async function processCss({ fileSystem, cssFiles, scopeClass, engine, mode = 'development' }) {
96
+ const assets = new Map();
97
+ let combined = '';
98
+ for (const file of cssFiles) {
99
+ const raw = fileSystem.readFile(file, 'utf8');
100
+ const result = await postcss([
101
+ ...(scopeClass ? [scopePlugin(scopeClass)] : [neutralizeGlobals()]),
102
+ urlRewrite(fileSystem, path.dirname(file), assets),
103
+ ]).process(raw, { from: file });
104
+ combined += result.css + '\n';
105
+ }
106
+ const css = mode === 'production' ? await minifyCss(engine, combined) : combined;
107
+ return { css, assets };
108
+ }
@@ -0,0 +1,184 @@
1
+ import { path } from './nodeApi.mjs';
2
+ import { packageDirOf } from './packageMeta.mjs';
3
+
4
+ // No logger is imported here: the console is a host capability, and discovery is portable.
5
+ // Warnings go into the injected `log`.
6
+ const SILENT = () => {};
7
+
8
+ // Module entry convention: <name>/<name>.<type>.(ts|tsx) (folder name === module name)
9
+ const MODULE_FILE_RE = /^(?<name>[a-z][A-Za-z0-9]*)\.(?<type>[a-z][A-Za-z0-9]*)\.(?:ts|tsx)$/;
10
+ // Content entities (pages/prefabs) follow the same lowerCamel naming (folder-per-entity).
11
+ const ENTITY_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
12
+
13
+ // Module types that are NOT built for the client distributive. `settings` is no module at all: a
14
+ // block's settings schema beside it, read by the build into the block's catalog entry.
15
+ const NON_CLIENT_TYPES = new Set(['server', 'router', 'settings']);
16
+
17
+ // A tile palette's config: `mosetta.json`. Its `brand` is whose tiles these are (ADR-0071) — on the
18
+ // wire it stays the namespace, the `_ns_` of every tile id, so the build carries it as `namespace`.
19
+ export function readConfig(fileSystem, root) {
20
+ const file = path.join(root, 'mosetta.json');
21
+ const cfg = fileSystem.readJson(file);
22
+ if (cfg && typeof cfg === 'object' && 'namespace' in cfg) {
23
+ throw new Error(`${file}: "namespace" is called "brand" now — rename the key`);
24
+ }
25
+ return cfg && typeof cfg === 'object' && typeof cfg.brand === 'string' ? { ...cfg, namespace: cfg.brand } : cfg;
26
+ }
27
+ function readPkg(fileSystem, root) {
28
+ return fileSystem.readJson(path.join(root, 'package.json'));
29
+ }
30
+
31
+ /** a distribution's version: plain X.Y.Z — a release is immutable, and its id must name it exactly */
32
+ const RELEASE_VERSION_RE = /^\d+\.\d+\.\d+$/;
33
+
34
+ /** the version a distribution is built under, or a build error saying what it must be */
35
+ export function releaseVersionOf(version, namespace, dir) {
36
+ if (typeof version !== 'string' || !RELEASE_VERSION_RE.test(version)) {
37
+ throw new Error(`tile palette ${namespace} (${dir}): version "${version}" is not X.Y.Z — a deployed version never changes, and its tile ids must name it exactly (pre-releases are not supported)`);
38
+ }
39
+ return version;
40
+ }
41
+
42
+ /**
43
+ * Distributive roots = the app itself (if type library) + `libraries` + `devLibraries` (dev only).
44
+ * Each: { dir, namespace, version, isDev }.
45
+ */
46
+ export function discoverRoots(fileSystem, root, { resolver, includeDev = true, log = SILENT } = {}) {
47
+ const cfg = readConfig(fileSystem, root);
48
+ const pkg = readPkg(fileSystem, root);
49
+ const roots = [];
50
+
51
+ if (cfg.type === 'library') {
52
+ roots.push({ dir: root, namespace: cfg.namespace, version: releaseVersionOf(pkg.version, cfg.namespace, root), isDev: false });
53
+ }
54
+
55
+ const resolveLib = (lib, isDev) => {
56
+ // editor & co. only export '.', so resolve the main entry then walk up to its package.json.
57
+ let mainPath;
58
+ try {
59
+ mainPath = resolver.resolve(lib, root);
60
+ } catch (e) {
61
+ log(` ! library ${lib} not resolved (${e.code || e.message}) — skipped`);
62
+ return null;
63
+ }
64
+ const dir = packageDirOf(fileSystem, mainPath);
65
+ if (!dir) {
66
+ log(` ! library ${lib}: package.json not found — skipped`);
67
+ return null;
68
+ }
69
+ const libCfg = readConfig(fileSystem, dir);
70
+ const libPkg = readPkg(fileSystem, dir);
71
+ return { dir, namespace: libCfg.namespace, version: releaseVersionOf(libPkg.version, libCfg.namespace, dir), isDev };
72
+ };
73
+
74
+ for (const lib of cfg.libraries || []) {
75
+ const r = resolveLib(lib, false);
76
+ if (r) roots.push(r);
77
+ }
78
+ if (includeDev) {
79
+ for (const lib of cfg.devLibraries || []) {
80
+ const r = resolveLib(lib, true);
81
+ if (r) roots.push(r);
82
+ }
83
+ }
84
+ return roots;
85
+ }
86
+
87
+ // Walk <root.dir>/tiles (the tile sources) for entries matching the naming convention whose type passes `keepType`.
88
+ // Returns [{ file, name, type, namespace, version }].
89
+ function collectModules(fileSystem, { dir, namespace, version }, keepType) {
90
+ const modulesDir = path.join(dir, 'tiles');
91
+ const out = [];
92
+ if (!fileSystem.exists(modulesDir)) return out;
93
+
94
+ const walk = (d) => {
95
+ for (const entry of fileSystem.readDir(d)) {
96
+ const p = path.join(d, entry.name);
97
+ if (entry.isDirectory) {
98
+ walk(p);
99
+ continue;
100
+ }
101
+ const m = MODULE_FILE_RE.exec(entry.name);
102
+ if (!m) continue;
103
+ if (path.basename(path.dirname(p)) !== m.groups.name) continue;
104
+ if (!keepType(m.groups.type)) continue;
105
+ out.push({ file: p, name: m.groups.name, type: m.groups.type, namespace, version });
106
+ }
107
+ };
108
+
109
+ walk(modulesDir);
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * Discover CLIENT module entries (everything except server-only types → the browser distributive).
115
+ */
116
+ export function discoverModulesIn(fileSystem, root) {
117
+ return collectModules(fileSystem, root, (type) => !NON_CLIENT_TYPES.has(type));
118
+ }
119
+
120
+ /**
121
+ * Discover SERVER-action module entries (`.server.ts`). Built as PRIVATE monoliths,
122
+ * executed in a Worker Loader isolate — not client artifacts.
123
+ */
124
+ export function discoverServerModulesIn(fileSystem, root) {
125
+ return collectModules(fileSystem, root, (type) => type === 'server');
126
+ }
127
+
128
+ /**
129
+ * Discover ROUTER facets (`.router.ts`) — only to refuse them: there is no router any more
130
+ * (ADR-0053), the build names a leftover facet instead of ignoring it.
131
+ */
132
+ export function discoverRouterModulesIn(fileSystem, root) {
133
+ return collectModules(fileSystem, root, (type) => type === 'router');
134
+ }
135
+
136
+ // Revisioned content entity: `<collection>/<name>/<metaFile>` + `versions/<rev>.json`.
137
+ // Only the LIVE (published) revision is distributed; entities without a live revision (drafts only)
138
+ // are editor-material and never reach the dist. Returns { meta, json } or null.
139
+ function readLiveEntity(fileSystem, collectionDir, name, metaFile, log) {
140
+ let meta;
141
+ try {
142
+ meta = fileSystem.readJson(path.join(collectionDir, name, metaFile));
143
+ } catch {
144
+ return null;
145
+ }
146
+ if (!meta || typeof meta.live !== 'number') return null;
147
+ let json;
148
+ try {
149
+ json = fileSystem.readJson(path.join(collectionDir, name, 'versions', `${meta.live}.json`));
150
+ } catch {
151
+ log(` ! ${name}: live revision ${meta.live} file missing/broken — skipped`);
152
+ return null;
153
+ }
154
+ return { meta, json };
155
+ }
156
+
157
+ function discoverEntitiesIn(fileSystem, dir, collection, metaFile, log) {
158
+ const collectionDir = path.join(dir, collection);
159
+ const out = [];
160
+ if (!fileSystem.exists(collectionDir)) return out;
161
+ for (const entry of fileSystem.readDir(collectionDir)) {
162
+ if (!entry.isDirectory || !ENTITY_NAME_RE.test(entry.name)) continue;
163
+ const live = readLiveEntity(fileSystem, collectionDir, entry.name, metaFile, log);
164
+ if (live) out.push({ name: entry.name, ...live });
165
+ }
166
+ return out;
167
+ }
168
+
169
+ /**
170
+ * Discover prefabs (live revisions) in <dir>/prefabs. Returns [{ name, type, json }] —
171
+ * type comes from the entity meta (it moved out of the file name).
172
+ */
173
+ export function discoverPrefabsIn(fileSystem, dir, log = SILENT) {
174
+ return discoverEntitiesIn(fileSystem, dir, 'prefabs', 'prefab.json', log)
175
+ .map(({ name, meta, json }) => ({ name, type: meta.type || 'unit', json }));
176
+ }
177
+
178
+ // Pages are a SEPARATE collection from prefabs (prefabs are reusable templates; a page is a routed,
179
+ // servable tree). Folder per page in `<root>/pages/<name>/` (meta + revisions). Edited by
180
+ // the content manager (in dev — coder via the local editor); in prod they live in S3.
181
+ export function discoverPagesIn(fileSystem, dir, log = SILENT) {
182
+ return discoverEntitiesIn(fileSystem, dir, 'pages', 'page.json', log)
183
+ .map(({ name, json }) => ({ name, json }));
184
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Build environment port: file system, READ-ONLY.
3
+ *
4
+ * The name is literal: the port answers exactly the questions about files. Package resolution
5
+ * is not part of it — that is a separate abstraction (`PackagesResolver`) ON TOP of the port:
6
+ * it needs files, but is itself policy over the package catalog, not an environment capability.
7
+ * Pure math — `node:path` and `node:crypto` — is deliberately outside the port: both modules
8
+ * exist in workerd under `nodejs_compat`, and wrapping them would breed an abstraction with no
9
+ * second implementation.
10
+ *
11
+ * The port ONLY READS: no writes, no execution. This is not tidiness but a property —
12
+ * the build isolate has no capability to change anything; artifacts leave as the return value.
13
+ *
14
+ * The contract is deliberately SYNCHRONOUS on reads: both rollup and postcss call these
15
+ * operations from their synchronous places. A network implementation is not hindered — it
16
+ * arrives with an already-warmed slice (the plan knows the closure in advance) instead of
17
+ * fetching each file at call time.
18
+ */
19
+ export class FileSystem {
20
+ /** Read a source file. `encoding: 'utf8'` → string, otherwise binary (Uint8Array-compatible). */
21
+ readFile(_file, _encoding = null) {
22
+ throw new Error('FileSystem.readFile is not implemented');
23
+ }
24
+
25
+ /** Read + parse JSON. The default is expressed via readFile — implementations need not override. */
26
+ readJson(file) {
27
+ return JSON.parse(this.readFile(file, 'utf8'));
28
+ }
29
+
30
+ exists(_file) {
31
+ throw new Error('FileSystem.exists is not implemented');
32
+ }
33
+
34
+ /** Directory listing as [{ name, isDirectory }]. */
35
+ readDir(_dir) {
36
+ throw new Error('FileSystem.readDir is not implemented');
37
+ }
38
+
39
+ /**
40
+ * Is this a directory. Needed by the resolver: `exists` does not tell a file from a directory,
41
+ * and the difference matters — with both `foo.js` and `foo/` present, node picks the FILE.
42
+ * The answer must be SELF-SUFFICIENT, not derived from other calls (spike lesson: on a
43
+ * case-insensitive FS `exists('src/Router')` answers for the `router/` directory, and a
44
+ * derivation from the file map diverges from the truth).
45
+ */
46
+ isDirectory(dir) {
47
+ try {
48
+ this.readDir(dir);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Canonical path of a file. On node this is symlink dereferencing (pnpm workspace layout);
57
+ * for a graph implementation it is identity: package identity is baked into the id, symlinks
58
+ * do not exist.
59
+ */
60
+ realPath(_file) {
61
+ throw new Error('FileSystem.realPath is not implemented');
62
+ }
63
+
64
+ /** Mtime in milliseconds, or null when the file is missing. Needed by freshness-validated caches. */
65
+ mtimeMs(_file) {
66
+ throw new Error('FileSystem.mtimeMs is not implemented');
67
+ }
68
+
69
+ }
package/src/format.mjs ADDED
@@ -0,0 +1,4 @@
1
+ /** Human-readable size for build reports. Pure function — touches no environment. */
2
+ export function formatBytes(bytes) {
3
+ return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
4
+ }
package/src/hash.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import { crypto, path } from './nodeApi.mjs';
2
+
3
+ // Short content hash for asset cache-busting (build-time; Node crypto is fine here).
4
+ export function shortHash(buffer) {
5
+ return crypto.createHash('sha1').update(buffer).digest('hex').slice(0, 8);
6
+ }
7
+
8
+ // `image.png` + <buf> -> `image_a1b2c3d4.png`
9
+ export function hashedAssetName(sourcePath, buffer) {
10
+ const ext = path.extname(sourcePath);
11
+ const base = path.basename(sourcePath, ext);
12
+ return `${base}_${shortHash(buffer)}${ext}`;
13
+ }
@@ -0,0 +1,81 @@
1
+ import { path } from './nodeApi.mjs';
2
+ import { FileSystem } from './fileSystem.mjs';
3
+
4
+ /**
5
+ * IN-MEMORY file port (build-action): the unpacked build body — a "path → bytes" map plus a
6
+ * directory index derived on insertion. Case is EXACT (unlike macOS — the `isDirectory` lesson:
7
+ * the port's answers are self-sufficient, nothing to derive). `realPath` is identity (symlinks
8
+ * do not exist in the plan), `mtimeMs` is null (freshness caches have nothing to validate: the
9
+ * plan is immutable).
10
+ */
11
+ export class MemoryFileSystem extends FileSystem {
12
+ constructor() {
13
+ super();
14
+ this.files = new Map(); // absolute path -> Uint8Array
15
+ this.children = new Map(); // directory -> Map(name -> isDirectory)
16
+ }
17
+
18
+ set(file, bytes) {
19
+ this.files.set(file, bytes);
20
+ let name = path.basename(file);
21
+ let dir = path.dirname(file);
22
+ let isDir = false;
23
+ for (;;) {
24
+ let listing = this.children.get(dir);
25
+ if (!listing) {
26
+ listing = new Map();
27
+ this.children.set(dir, listing);
28
+ }
29
+ const known = listing.has(name);
30
+ listing.set(name, listing.get(name) || isDir);
31
+ if (known) break;
32
+ name = path.basename(dir);
33
+ const parent = path.dirname(dir);
34
+ if (parent === dir) break;
35
+ dir = parent;
36
+ isDir = true;
37
+ }
38
+ }
39
+
40
+ readFile(file, encoding = null) {
41
+ const bytes = this.files.get(file);
42
+ if (bytes === undefined) {
43
+ const e = new Error(`ENOENT: ${file}`);
44
+ e.code = 'ENOENT';
45
+ throw e;
46
+ }
47
+ return encoding ? new TextDecoder().decode(bytes) : bytes;
48
+ }
49
+
50
+ exists(file) {
51
+ return this.files.has(file) || this.children.has(file);
52
+ }
53
+
54
+ readDir(dir) {
55
+ const listing = this.children.get(dir);
56
+ if (!listing) {
57
+ const e = new Error(`ENOENT: ${dir} is not a directory`);
58
+ e.code = 'ENOENT';
59
+ throw e;
60
+ }
61
+ return [...listing].map(([name, isDirectory]) => ({ name, isDirectory }));
62
+ }
63
+
64
+ isDirectory(dir) {
65
+ return this.children.has(dir);
66
+ }
67
+
68
+ realPath(file) {
69
+ if (!this.exists(file)) {
70
+ const e = new Error(`ENOENT: ${file}`);
71
+ e.code = 'ENOENT';
72
+ throw e;
73
+ }
74
+ return file;
75
+ }
76
+
77
+ mtimeMs() {
78
+ return null;
79
+ }
80
+
81
+ }
@@ -0,0 +1,11 @@
1
+ // Canonical module-name encoding: _ns_<namespace>_t_<type>_n_<name>_v_<version>
2
+ // Pure naming primitives live in @mosetta/runtime (browser-safe, shared with the runtime resolver).
3
+ import { normalizeVersion } from '@mosetta/runtime/naming';
4
+
5
+ export { normalizeVersion, dependencyFullId } from '@mosetta/runtime/naming';
6
+
7
+ export const NAME_TOKENS = { ns: '_ns_', t: '_t_', n: '_n_', v: '_v_' };
8
+
9
+ export function fullTileName({ namespace, type, name, version }) {
10
+ return `${NAME_TOKENS.ns}${namespace}${NAME_TOKENS.t}${type}${NAME_TOKENS.n}${name}${NAME_TOKENS.v}${normalizeVersion(version)}`;
11
+ }
package/src/node.mjs ADDED
@@ -0,0 +1,113 @@
1
+ // THE NODE HOST of the build: what a host running in node gives the build — the real file system
2
+ // (read only), the framework's files resolved from this installation, and the build of a palette
3
+ // that lies on disk with its dependencies installed. Shared by the CLI and the platform's build
4
+ // container; anything that runs node builds the same way.
5
+ //
6
+ // Nothing here fetches anything: the dependencies are installed by whoever calls (a developer's
7
+ // pnpm, CI, a build container), the build only reads them.
8
+ //
9
+ // The one entry of the package that is node's: the port's node implementation, an entry of its
10
+ // own (`@mosetta/build/node`) that the build's core never imports.
11
+ /* eslint-disable no-restricted-imports -- the node HOST: the file port and resolution, for node */
12
+ import fs from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import path from 'node:path';
15
+ import { build, INERT_BUILD_CONFIG } from './build.mjs';
16
+ import { FileSystem } from './fileSystem.mjs';
17
+ import { FRAMEWORK_RESOLUTIONS, PackagesResolver, packageDirOf } from './packagesResolver.mjs';
18
+ import { BUILD_CONFIG_FILE, scanCatalog } from './scanner.mjs';
19
+ /* eslint-enable no-restricted-imports */
20
+
21
+ /**
22
+ * Node implementation of the file port: the real FS, read-only.
23
+ *
24
+ * Packages are resolved by `PackagesResolver` from the scanner catalog, identically in all
25
+ * environments — not here.
26
+ */
27
+ export class NodeFileSystem extends FileSystem {
28
+ readFile(file, encoding = null) {
29
+ return encoding ? fs.readFileSync(file, encoding) : fs.readFileSync(file);
30
+ }
31
+
32
+ exists(file) {
33
+ return fs.existsSync(file);
34
+ }
35
+
36
+ readDir(dir) {
37
+ return fs.readdirSync(dir, { withFileTypes: true })
38
+ .map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
39
+ }
40
+
41
+ isDirectory(dir) {
42
+ try {
43
+ return fs.statSync(dir).isDirectory();
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ realPath(file) {
50
+ return fs.realpathSync(file);
51
+ }
52
+
53
+ mtimeMs(file) {
54
+ try {
55
+ return fs.statSync(file).mtimeMs;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ }
61
+
62
+ const buildRequire = createRequire(import.meta.url);
63
+
64
+ /** a framework file (`systemjs/dist/system.js`, `@mosetta/runtime/bootstrap`) as this installation of the build resolves it */
65
+ export function frameworkFileOf(specifier) {
66
+ return buildRequire.resolve(specifier);
67
+ }
68
+
69
+ /** `catalog.framework`: the files of FRAMEWORK_RESOLUTIONS from this installation of the build */
70
+ export function frameworkResolutions() {
71
+ const framework = {};
72
+ for (const spec of FRAMEWORK_RESOLUTIONS) framework[spec] = frameworkFileOf(spec);
73
+ return framework;
74
+ }
75
+
76
+ /**
77
+ * The directory generated sides' imports (the default ssr) resolve from: this installation of the
78
+ * build, where @mosetta/router, @mosetta/server and react are declared.
79
+ */
80
+ export function frameworkDirOf(fileSystem = new NodeFileSystem()) {
81
+ return packageDirOf(fileSystem, frameworkFileOf('@mosetta/build'));
82
+ }
83
+
84
+ /**
85
+ * The build of the palette at `root` — on disk, its dependencies installed — with THIS build: the
86
+ * one installed for the palette, so a palette is built by the framework version its lock names.
87
+ * `engine` — the host's bundling engine; `evaluate` — the host's way to run a module the build
88
+ * bundled (settings schemas); `released` — ids the target already holds, not built again. A palette
89
+ * with build hooks (mosetta.build.mjs) needs its `buildConfig` made executable by the host; without
90
+ * one the build is refused rather than made without its hooks.
91
+ */
92
+ export async function buildDirectory(root, { mode = 'production', engine, log = () => {}, evaluate = null, released = null, buildConfig = null } = {}) {
93
+ const fileSystem = new NodeFileSystem();
94
+ if (!buildConfig && fileSystem.exists(path.join(root, BUILD_CONFIG_FILE))) {
95
+ throw new Error(`the tile palette uses ${BUILD_CONFIG_FILE} (build hooks), but no executable hooks were passed`);
96
+ }
97
+ const frameworkDir = frameworkDirOf(fileSystem);
98
+ const resolver = new PackagesResolver({
99
+ fileSystem,
100
+ catalog: { ...scanCatalog({ fileSystem, root, frameworkDir, includeDev: mode !== 'production', log }), framework: frameworkResolutions() },
101
+ });
102
+ return build(root, {
103
+ mode,
104
+ log,
105
+ fileSystem,
106
+ resolver,
107
+ engine,
108
+ frameworkDir,
109
+ buildConfig: buildConfig ?? INERT_BUILD_CONFIG,
110
+ ...(evaluate ? { evaluate } : {}),
111
+ ...(released ? { released } : {}),
112
+ });
113
+ }