@mosetta/cli 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 ADDED
@@ -0,0 +1,142 @@
1
+ # @mosetta/cli
2
+
3
+ ## 0.0.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 95b93d5: The build's node host is an entry of the build: `@mosetta/build/node` gives the real file system,
8
+ the framework's files resolved from the installed build, and `buildDirectory(root, …)` — the build
9
+ of a palette that lies on disk with its dependencies installed, by the build installed for it. The
10
+ CLI builds with it, and so does a platform's build container: a palette is built by the framework
11
+ version its lock names. `systemjs`, whose files the build mixes into a page, is a dependency of the
12
+ build now, not of the CLI.
13
+ - Updated dependencies [95b93d5]
14
+ - @mosetta/build@0.0.10
15
+ - @mosetta/stores@0.0.10
16
+ - @mosetta/runtime@0.0.10
17
+ - @mosetta/server@0.0.10
18
+
19
+ ## 0.0.9
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies
24
+ - @mosetta/runtime@0.0.9
25
+ - @mosetta/build@0.0.9
26
+ - @mosetta/server@0.0.9
27
+ - @mosetta/stores@0.0.9
28
+
29
+ ## 0.0.8
30
+
31
+ ### Patch Changes
32
+
33
+ - A project's own tile palette. The registry keeps the palettes a project's site takes tiles from;
34
+ the factory reads each one's tile host after the platform's, and takes a tile of a host other than
35
+ the primary only with its integrity. The rule a site's tile hosts merge by lives in
36
+ `@mosetta/core` (`mergeTileHosts`), so the editor offers the blocks of every tile host of the site
37
+ by the same rule (`TILE_HOSTS_TOKEN` in `@mosetta/server-action`). The platform's API publishes
38
+ into `palettes/<brand>` for the members of the project of that namespace, with the palette's stores
39
+ granted to that one request. `mosetta build --released <descriptor>` leaves out what a platform
40
+ already holds; `checkPaletteBuild` says what a palette may hold. The CLI's and the build's output
41
+ speaks the vocabulary: tile palettes, tiles, package tiles.
42
+ - Updated dependencies
43
+ - @mosetta/server@0.0.8
44
+ - @mosetta/build@0.0.8
45
+ - @mosetta/runtime@0.0.8
46
+ - @mosetta/stores@0.0.8
47
+
48
+ ## 0.0.7
49
+
50
+ ### Patch Changes
51
+
52
+ - One vocabulary: code is tiles, content is blueprints, running requests is the factory, and a tile and a
53
+ blueprint made in the factory give a mosaic. A tile is a folder under `tiles/` (was `modules/`) whose
54
+ files are its sides, each defined with `define<Side>Side` — `defineComponentSide`, `defineEditorSide`,
55
+ `defineFallbackSide`, `defineRootComponentSide`, `defineServerSide`, `defineSsrSide` (were
56
+ `define…Module`); `useTile` (was `useModule`), `markInternalTile`, `BlueprintRoot`,
57
+ `BlueprintNodeRenderer`. `mosetta.json` names the tile `brand` (was `namespace`; a config that still
58
+ says `namespace` is refused with the word to use). The store connectors' package is `@mosetta/stores`
59
+ (was `@mosetta/provider`): `IStoreConnector`, `StoreConnectors`, `tileHostsOf`. The server's runner
60
+ is the factory gate (`@mosetta/server/factoryGate`, was `/runner`). Nothing on the wire moves: tile
61
+ ids keep `_ns_`, a build keeps `modules/<id>/`, stored JSON keeps its fields.
62
+ - Updated dependencies
63
+ - @mosetta/runtime@0.0.7
64
+ - @mosetta/server@0.0.7
65
+ - @mosetta/build@0.0.7
66
+ - @mosetta/stores@0.0.7
67
+
68
+ ## 0.0.6
69
+
70
+ ### Patch Changes
71
+
72
+ - Updated dependencies
73
+ - @mosetta/provider@0.0.6
74
+ - @mosetta/server@0.0.6
75
+ - @mosetta/runtime@0.0.6
76
+ - @mosetta/build@0.0.6
77
+
78
+ ## 0.0.5
79
+
80
+ ### Patch Changes
81
+
82
+ - Updated dependencies [92417dc]
83
+ - @mosetta/server@0.0.5
84
+ - @mosetta/build@0.0.5
85
+ - @mosetta/provider@0.0.5
86
+ - @mosetta/runtime@0.0.5
87
+
88
+ ## 0.0.4
89
+
90
+ ### Patch Changes
91
+
92
+ - 7577aa3: A server action asks the project's secrets by name — `SECRETS_TOKEN` (`get(name)` → the value or null) — and reads the
93
+ request it answers — `REQUEST_TOKEN` (its address and headers). The host grants each server module's isolate a
94
+ capability for its own scopes (`/`, `/<namespace>`, `/<namespace>/<module>`, the narrowest wins); no value sits in an
95
+ isolate's env, and pages get none. The registry keeps a project's secrets and plain settings, sealed with AES-GCM under
96
+ a root key the host provides. `mosetta dev` reads them from `.mosetta/secrets.json`.
97
+ - Updated dependencies [7577aa3]
98
+ - Updated dependencies [7577aa3]
99
+ - @mosetta/server@0.0.4
100
+ - @mosetta/build@0.0.4
101
+ - @mosetta/provider@0.0.4
102
+ - @mosetta/runtime@0.0.4
103
+
104
+ ## 0.0.3
105
+
106
+ ### Patch Changes
107
+
108
+ - a8d5a2c: A person acts on the platform from a program with a platform token: `mosetta login` signs the CLI in by a code the
109
+ person approves in the browser (a device flow), and keeps the token per installation in their config folder, readable
110
+ by them only; `MOSETTA_TOKEN` wins over it. The registry keeps tokens as hashes only, and the CLI's sign-in requests;
111
+ the cabinet's Worker makes, lists and revokes tokens and approves sign-ins.
112
+ - 1330d5d: Code is published THROUGH a platform: `mosetta push` describes the build (both catalogs, every file with its SHA-256,
113
+ the commit it comes from), asks the platform's API for a plan and sends the files it asks for — no store key on the
114
+ publisher's side. The release rules check a fresh module's bytes against the catalog entry that vouches for them, on
115
+ either path. A runner can grant stores to server actions only (found by name), never read for its own modules — how
116
+ the platform's API writes code stores whose keys stay with its host; the host names the publish target in a header of
117
+ its own, beside the caller's identity.
118
+ - 886c43a: The release rules live in `@mosetta/build/release`, portable, for every publisher: `mosetta deploy` runs them against a
119
+ provider registry, a platform can run them on its side. A release writes each catalog over exactly the copy it planned
120
+ from and a module file only where none is, so two deploys into one store at once can no longer drop each other's
121
+ entries — the one overtaken plans again. A provider write takes a condition (`ifMatch`, `ifNoneMatch: '*'`), answered
122
+ 412 when someone else got there first.
123
+ - Updated dependencies [1330d5d]
124
+ - Updated dependencies [886c43a]
125
+ - Updated dependencies [842344f]
126
+ - @mosetta/build@0.0.3
127
+ - @mosetta/provider@0.0.3
128
+ - @mosetta/server@0.0.3
129
+ - @mosetta/runtime@0.0.3
130
+
131
+ ## 0.0.2
132
+
133
+ ### Patch Changes
134
+
135
+ - The first versioned release. A deployed version never changes, and the stores keep every version
136
+ deployed. A page runs one copy of each framework package — the newest its blocks were built with,
137
+ or the version its root pins; the Framework distribution's updater lifts a page to its framework.
138
+ - Updated dependencies
139
+ - @mosetta/server@0.0.2
140
+ - @mosetta/build@0.0.2
141
+ - @mosetta/provider@0.0.2
142
+ - @mosetta/runtime@0.0.2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mosetta contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # @mosetta/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `mosetta` command: a development server and the build of a deployable tile palette.
4
+
5
+ ```
6
+ mosetta dev [--root <dir>] [--port <n>] [--write] [--no-typecheck]
7
+ mosetta build [--root <dir>] [--out <dir>] [--no-typecheck] [--released <descriptor url|file>]
8
+ mosetta push [--root <dir>] [--target <name>] [--dry-run]
9
+ mosetta login [--target <name>]
10
+ mosetta version <patch|minor|major> [--package <brand|name>]
11
+ ```
12
+
13
+ - `dev` builds in memory and serves everything on one port — the factory in workerd, static
14
+ assets, the content write channel and live reload — while node only builds and watches files.
15
+ - `build` writes `dist/public/` (what browsers may load: tile side artifacts, package tiles,
16
+ stylesheets, the tile host descriptor) and `dist/private/` (server sides, root renderers), gated
17
+ on a clean type check. `--released` leaves out what a platform already holds.
18
+ - `push` releases the build through a platform's API: no store key on the machine, the platform
19
+ checks every byte against the build's manifest.
20
+
21
+ A site is a folder with `mosetta.json` (its brand and the tile palettes it uses), `tiles/` and
22
+ `pages/`.
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { startDev } from '../src/dev.mjs';
4
+ import { releasedIdsOf, staticBuild } from '../src/staticBuild.mjs';
5
+ import { deploy } from '../src/deploy.mjs';
6
+ import { raiseVersion } from '../src/version.mjs';
7
+ import { forget, login, publishTargetOf } from '../src/login.mjs';
8
+ import { push } from '../src/push.mjs';
9
+ import { runTypecheck } from '../src/typecheck.mjs';
10
+ import { NodeFileSystem } from '../src/host/nodeFileSystem.mjs';
11
+ import { frameworkFileOf } from '../src/scanner.mjs';
12
+ import { packageDirOf } from '@mosetta/build/packagesResolver';
13
+ import { loadEngine } from '../src/engineLoader.mjs';
14
+ import { sectionLog } from '../src/log.mjs';
15
+
16
+ const args = process.argv.slice(2);
17
+ const cmd = args[0];
18
+
19
+ const flag = (name) => args.includes(name);
20
+ const opt = (name, dflt) => {
21
+ const i = args.indexOf(name);
22
+ return i >= 0 ? args[i + 1] : dflt;
23
+ };
24
+
25
+ const root = path.resolve(opt('--root', process.cwd()));
26
+
27
+ // Binding the build to the environment happens HERE and only here: the builder itself has no
28
+ // defaults, otherwise a default silently drags node back into portable code.
29
+ // fileSystem — where sources come from (one implementation so far: disk); the package resolver
30
+ // is rebuilt BEFORE EVERY build iteration from the scanner catalog — inside dev/build
31
+ // engine — what builds the module graph. ONE for all environments: wasm (`@rollup/browser` +
32
+ // `esbuild-wasm`) — the same set that ships into the isolate. The framework deliberately has no
33
+ // native binaries (per-OS platform builds, CI matrices and differential verification of two
34
+ // engines are a class of problems we do not have to own). Extra speed comes via the
35
+ // `mosetta.engine.mjs` hook (see engineLoader.mjs).
36
+ const fileSystem = new NodeFileSystem();
37
+ // The directory GENERATED facets' imports (default ssr) resolve from: the @mosetta/build
38
+ // installation, where @mosetta/router, @mosetta/server and react are declared.
39
+ // Computed HERE and passed down — the builder must not derive its own location.
40
+ const frameworkDir = packageDirOf(fileSystem, frameworkFileOf('@mosetta/build'));
41
+ // only the commands that build need the engine — a wasm service left running keeps the others
42
+ // from exiting cleanly
43
+ const engine = cmd === 'build' || cmd === 'dev'
44
+ ? await loadEngine(root, { mode: cmd === 'build' ? 'production' : 'development', log: sectionLog('cli') })
45
+ : null;
46
+
47
+ switch (cmd) {
48
+ case 'build':
49
+ // types gate the PROD build only (dev stays fast): tsc --noEmit against the app tsconfig
50
+ if (!flag('--no-typecheck') && !(await runTypecheck({ root }))) {
51
+ sectionLog('cli')('build: type errors above — nothing emitted (--no-typecheck to bypass)');
52
+ process.exit(1);
53
+ }
54
+ // build the deployable tile palette → <root>/dist (artifacts + pages→HTML + pages.json);
55
+ // --released <descriptor url|file>: what is there already is not built (a project's palette
56
+ // over the platform's tiles)
57
+ await staticBuild(root, {
58
+ outDir: opt('--out', undefined),
59
+ fileSystem,
60
+ engine,
61
+ frameworkDir,
62
+ released: opt('--released', undefined) ? await releasedIdsOf(opt('--released', undefined)) : null,
63
+ });
64
+ break;
65
+ case 'dev': {
66
+ // one public port: workerd holds render, static assets and the content channel; node builds
67
+ // and watches files, opening no sockets. `--port` picks the address; a busy port is
68
+ // diagnosed at startup (most often a copy that survived a previous run).
69
+ // in-memory build by default; --write (or --emit) mirrors the raw build to <root>/dist for inspection.
70
+ const write = flag('--write') || flag('--emit');
71
+ const port = Number(opt('--port', 3000));
72
+ await startDev({ root, port, write, typecheck: !flag('--no-typecheck'), fileSystem, engine, frameworkDir });
73
+ break;
74
+ }
75
+ case 'deploy': {
76
+ // the built distribution → the project's stores by the provider registry (S3-compatible:
77
+ // R2, MinIO, …); --content seeds pages/prefabs/config.json without overwriting edits.
78
+ // --target <name> picks <root>/.mosetta/providers.<name>.json (a stage beside prod); without
79
+ // it — the default registry, never another installation by accident
80
+ const target = opt('--target', undefined);
81
+ if (target !== undefined && !/^[a-z0-9-]+$/.test(target)) {
82
+ sectionLog('cli')(`deploy: --target "${target}" is not a name (a-z, 0-9, -)`);
83
+ process.exitCode = 1;
84
+ break;
85
+ }
86
+ try {
87
+ const summary = await deploy(root, {
88
+ registryFile: opt('--registry', target ? path.join(root, '.mosetta', `providers.${target}.json`) : undefined),
89
+ content: flag('--content') || flag('--force-content'),
90
+ forceContent: flag('--force-content'),
91
+ contentOnly: flag('--content-only'),
92
+ dryRun: flag('--dry-run'),
93
+ });
94
+ sectionLog('cli')(`deploy: ${JSON.stringify(summary)}`);
95
+ } catch (e) {
96
+ // a refused release is an answer, not a crash: said plainly, exit code set
97
+ sectionLog('cli')(e.message);
98
+ process.exitCode = 1;
99
+ }
100
+ break;
101
+ }
102
+ case 'push': {
103
+ // the built distribution released THROUGH the platform's API: no store key here — the token
104
+ // `mosetta login` kept, the target from mosetta.json's publish.<--target|default>
105
+ try {
106
+ const release = await push(root, { target: opt('--target', 'default'), dryRun: flag('--dry-run') });
107
+ sectionLog('cli')(`push: ${JSON.stringify(release)}`);
108
+ } catch (e) {
109
+ sectionLog('cli')(e.message);
110
+ process.exitCode = 1;
111
+ }
112
+ break;
113
+ }
114
+ case 'login':
115
+ case 'logout': {
116
+ // a platform's token for this CLI: approved in the browser (a code, like `gh auth login`),
117
+ // kept in the person's config folder per installation; --target picks mosetta.json's
118
+ // publish.<name> (default: publish.default), --cabinet <origin> names one outright
119
+ try {
120
+ const { cabinet } = publishTargetOf(root, opt('--target', 'default'), { cabinet: opt('--cabinet', undefined) });
121
+ if (cmd === 'login') await login(cabinet, { open: flag('--no-browser') ? () => undefined : undefined });
122
+ else sectionLog('cli')(forget(cabinet) ? `signed out of ${cabinet} here — revoke the token in the cabinet's access tokens to end it everywhere` : `not signed in to ${cabinet}`);
123
+ } catch (e) {
124
+ sectionLog('cli')(e.message);
125
+ process.exitCode = 1;
126
+ }
127
+ break;
128
+ }
129
+ case 'version':
130
+ // a deployed version never changes: a change ships under a new one — `mosetta version patch
131
+ // --package mosetta-ui` raises it in the distribution's package.json
132
+ try {
133
+ raiseVersion(root, { target: opt('--package', undefined), level: args[1] });
134
+ } catch (e) {
135
+ sectionLog('cli')(`version: ${e.message}`);
136
+ process.exitCode = 1;
137
+ }
138
+ break;
139
+ default:
140
+ sectionLog('cli')('usage: mosetta <dev|build|deploy|push|login|logout|version> [--root <dir>] [--port <n>] [--write] [--out <dir>] [--no-typecheck] [--released <descriptor url|file>] [--registry <file> | --target <name>] [--content|--force-content|--content-only] [--dry-run]; mosetta version <patch|minor|major> [--package <brand|name>]');
141
+ }
package/package.json CHANGED
@@ -1,6 +1,24 @@
1
1
  {
2
2
  "name": "@mosetta/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.10",
4
+ "license": "MIT",
5
+ "description": "mosetta CLI: development server (single port, live reload, type-check companion), building a deployable tile palette and publishing it.",
6
+ "type": "module",
7
+ "bin": {
8
+ "mosetta": "./bin/mosetta.mjs"
9
+ },
10
+ "dependencies": {
11
+ "@rollup/browser": "4.62.3",
12
+ "chokidar": "^4.0.3",
13
+ "esbuild-wasm": "0.24.2",
14
+ "systemjs": "^6.15.1",
15
+ "workerd": "^1.20260630.1",
16
+ "@mosetta/stores": "0.0.10",
17
+ "@mosetta/build": "0.0.10",
18
+ "@mosetta/runtime": "0.0.10",
19
+ "@mosetta/server": "0.0.10"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
6
24
  }
@@ -0,0 +1,144 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { writeTar, gzip, SCAN_ENTRY } from '@mosetta/build/archive';
4
+ import { loadLockEdges, scanCatalog, scanToolingCatalog } from '@mosetta/build/scanner';
5
+ import { packageDirOf } from '@mosetta/build/packagesResolver';
6
+ import { NodeFileSystem } from './host/nodeFileSystem.mjs';
7
+ import { frameworkResolutions } from './scanner.mjs';
8
+
9
+ /**
10
+ * Packer for the build-action body — the trusted node side.
11
+ *
12
+ * The virtual world mirrors the layout RELATIVE to the outermost lockfile: base → /repo, every
13
+ * closure package and the sources at their own relative paths. This keeps the module graph's
14
+ * relative paths identical to disk, so isolate artifacts are byte-equal to a local build. The
15
+ * container carries: a gzip-tar of the sources, a gzip-tar per closure package (in prod these
16
+ * would be ready-made registry archives — the packer would not recreate them), meta.tgz with
17
+ * package.json/locks of the walk-up contexts, and the scan.json manifest.
18
+ */
19
+
20
+ // tenant SOURCES also drop dist (a build product); PACKAGES keep their dist (shipped code:
21
+ // systemjs/dist) and carry what a registry archive carries: the node_modules INSIDE a package are
22
+ // its bundledDependencies only (lightningcss-wasm ships napi-wasm this way; the scanner derives
23
+ // their edges from the lock) — anything else there (a workspace package's links, leftovers of
24
+ // another layout) is not the package's, and its own dependencies ship as packages of their own
25
+ const SKIP_SOURCE_DIRS = new Set(['node_modules', 'dist', '.git']);
26
+ const SKIP_PACKAGE_DIRS = new Set();
27
+ const VIRTUAL_BASE = '/repo';
28
+
29
+ /** the dependencies a package ships inside itself — `bundledDependencies` (or its older spelling) */
30
+ function bundledOf(dir) {
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
33
+ const named = pkg.bundledDependencies ?? pkg.bundleDependencies ?? [];
34
+ return new Set(Array.isArray(named) ? named : named === true ? Object.keys(pkg.dependencies ?? {}) : []);
35
+ } catch {
36
+ return new Set();
37
+ }
38
+ }
39
+
40
+ function filesOfDir(dir, skip, { bundled = null } = {}) {
41
+ const entries = [];
42
+ const walk = (d, rel) => {
43
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
44
+ if (e.name.startsWith('.') && e.name !== '.npmrc') continue;
45
+ const abs = path.join(d, e.name);
46
+ const r = rel ? `${rel}/${e.name}` : e.name;
47
+ if (e.isDirectory()) {
48
+ if (skip.has(e.name)) continue;
49
+ // a package's own node_modules: its bundled dependencies, by name, and nothing else
50
+ if (bundled && r === 'node_modules') {
51
+ for (const name of bundled) {
52
+ const at = path.join(abs, ...name.split('/'));
53
+ if (fs.existsSync(at) && fs.statSync(at).isDirectory()) walk(at, `node_modules/${name}`);
54
+ }
55
+ continue;
56
+ }
57
+ walk(abs, r);
58
+ } else if (e.isFile()) {
59
+ entries.push({ name: r, data: fs.readFileSync(abs) });
60
+ }
61
+ }
62
+ };
63
+ walk(dir, '');
64
+ return entries;
65
+ }
66
+
67
+ export async function packBuildArchive({ root, frameworkDir, mode = 'production' }) {
68
+ const fileSystem = new NodeFileSystem();
69
+ const { lockfileDirs } = loadLockEdges(fileSystem, root);
70
+ const base = fs.realpathSync(lockfileDirs[lockfileDirs.length - 1]); // outermost lock = world root
71
+ const toVirtual = (p) => {
72
+ const real = fs.realpathSync(p);
73
+ if (real === base) return VIRTUAL_BASE;
74
+ if (!real.startsWith(base + path.sep)) throw new Error(`outside the lock world: ${p}`);
75
+ return `${VIRTUAL_BASE}/${path.relative(base, real).split(path.sep).join('/')}`;
76
+ };
77
+
78
+ const includeDev = mode !== 'production';
79
+ const catalog = scanCatalog({ fileSystem, root, frameworkDir, includeDev });
80
+ // the tooling closure (build config) ships too: PHASE 1 inside the isolate assembles it,
81
+ // and devDependencies are legitimate there
82
+ const tooling = scanToolingCatalog({ fileSystem, root, frameworkDir });
83
+ const framework = frameworkResolutions();
84
+
85
+ // closure packages: edge targets + owners of framework files; walk-up contexts (ancestors of
86
+ // the root) ship as metadata only
87
+ const dirs = new Set();
88
+ for (const bucketSet of [catalog.edges, tooling.edges]) {
89
+ for (const [dir, bucket] of Object.entries(bucketSet)) {
90
+ dirs.add(dir);
91
+ for (const target of Object.values(bucket)) dirs.add(target);
92
+ }
93
+ }
94
+ for (const file of Object.values(framework)) {
95
+ const owner = packageDirOf(fileSystem, file);
96
+ if (owner) dirs.add(owner);
97
+ }
98
+ const realRoot = fs.realpathSync(root);
99
+ const isContext = (d) => realRoot === d || realRoot.startsWith(d + path.sep);
100
+
101
+ const outer = [];
102
+ const unpack = [];
103
+ const sources = writeTar(filesOfDir(realRoot, SKIP_SOURCE_DIRS));
104
+ outer.push({ name: 'sources.tgz', data: await gzip(sources) });
105
+ unpack.push({ entry: 'sources.tgz', dir: toVirtual(realRoot), strip: 0 });
106
+
107
+ const meta = [];
108
+ let i = 0;
109
+ // the lock also declares UNinstalled optional packages (platform binaries for other OSes) —
110
+ // absent on disk, so absent in the body; inside the isolate their absence behaves as on disk
111
+ const installed = [...dirs].filter((d) => fs.existsSync(d));
112
+ for (const d of installed.map((d) => fs.realpathSync(d)).sort()) {
113
+ if (d === realRoot) continue;
114
+ if (isContext(d)) {
115
+ for (const f of ['package.json', 'pnpm-lock.yaml', 'package-lock.json']) {
116
+ const abs = path.join(d, f);
117
+ if (fs.existsSync(abs)) {
118
+ meta.push({ name: `${toVirtual(d)}/${f}`.slice(VIRTUAL_BASE.length + 1) || f, data: fs.readFileSync(abs) });
119
+ }
120
+ }
121
+ continue;
122
+ }
123
+ const entry = `packages/${i++}.tgz`;
124
+ outer.push({ name: entry, data: await gzip(writeTar(filesOfDir(d, SKIP_PACKAGE_DIRS, { bundled: bundledOf(d) }))) });
125
+ unpack.push({ entry, dir: toVirtual(d), strip: 0 });
126
+ }
127
+ // base is itself a context: its lock is mandatory
128
+ for (const f of ['pnpm-lock.yaml', 'package-lock.json']) {
129
+ const abs = path.join(base, f);
130
+ if (fs.existsSync(abs) && !meta.some((m) => m.name === f)) meta.push({ name: f, data: fs.readFileSync(abs) });
131
+ }
132
+ outer.push({ name: 'meta.tgz', data: await gzip(writeTar(meta)) });
133
+ unpack.push({ entry: 'meta.tgz', dir: VIRTUAL_BASE, strip: 0 });
134
+
135
+ const manifest = {
136
+ root: toVirtual(realRoot),
137
+ frameworkDir: toVirtual(frameworkDir),
138
+ framework: Object.fromEntries(Object.entries(framework).map(([spec, file]) => [spec, toVirtual(file)])),
139
+ mode,
140
+ unpack,
141
+ };
142
+ outer.unshift({ name: SCAN_ENTRY, data: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) });
143
+ return writeTar(outer);
144
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { INERT_BUILD_CONFIG } from '@mosetta/build';
5
+
6
+ // mosetta.build.mjs: build extension hooks. `mosetta.json` is pure data, while build CODE lives in
7
+ // this co-located file. Loading cache-busts by mtime (the ESM import cache is permanent, a
8
+ // re-import needs a fresh URL) — config edits are picked up by the dev server without a restart.
9
+ //
10
+ // Lives in the CLI, not in @mosetta/build, BECAUSE this executes foreign code from a file —
11
+ // i.e. an environment capability. The build receives the result AS DATA and executes nothing
12
+ // itself; that is what kept the host port read-only.
13
+
14
+ const asFn = (v, name, file) => {
15
+ if (v === undefined || v === null) return null;
16
+ if (typeof v !== 'function') throw new Error(`mosetta.build.mjs: \`${name}\` must be a function (${file})`);
17
+ return v;
18
+ };
19
+
20
+ export async function loadBuildConfig(root) {
21
+ const file = path.join(root, 'mosetta.build.mjs');
22
+ let mtimeMs;
23
+ try {
24
+ mtimeMs = fs.statSync(file).mtimeMs;
25
+ } catch {
26
+ return { ...INERT_BUILD_CONFIG, file };
27
+ }
28
+ const ns = await import(`${pathToFileURL(file).href}?v=${mtimeMs}`);
29
+ const cfg = ns.default ?? {};
30
+ if (typeof cfg !== 'object') throw new Error(`mosetta.build.mjs must default-export an object of hooks (${file})`);
31
+ return {
32
+ file,
33
+ stamp: String(mtimeMs),
34
+ clientPlugins: asFn(cfg.clientPlugins, 'clientPlugins', file),
35
+ serverEsbuild: asFn(cfg.serverEsbuild, 'serverEsbuild', file),
36
+ rollupOptions: asFn(cfg.rollupOptions, 'rollupOptions', file),
37
+ };
38
+ }
39
+
40
+ /** Mirror a build snapshot to disk — also the host's side, not the build's. */
41
+ export function mirrorArtifacts(files, dir) {
42
+ fs.rmSync(dir, { recursive: true, force: true });
43
+ for (const [rel, content] of files) {
44
+ const p = path.join(dir, rel);
45
+ fs.mkdirSync(path.dirname(p), { recursive: true });
46
+ fs.writeFileSync(p, content);
47
+ }
48
+ }
@@ -0,0 +1,11 @@
1
+ import { bundleServer, SERVER_WORKER_ENTRY, SERVER_DEV_WORKER_ENTRY } from './serverPackage.mjs';
2
+
3
+ // Bundle the @mosetta/server worker (TS, OOP) into a single ESM module for workerd. Web API only
4
+ // (platform 'neutral') — runs unchanged in the Workers runtime.
5
+ //
6
+ // `dev: true` takes a DIFFERENT entry — `devWorker.ts`: the same render plus static serving from
7
+ // a Durable Object and the rebuild control channel (one port in dev). The prod worker knows
8
+ // nothing about it: dev knowledge lives in a separate facet, not in a flag inside the runner.
9
+ export async function buildRenderWorker({ mode = 'development', dev = false } = {}) {
10
+ return bundleServer(dev ? SERVER_DEV_WORKER_ENTRY : SERVER_WORKER_ENTRY, { platform: 'neutral', mode });
11
+ }