@mosetta/cli 0.0.0-stage → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.md +21 -2
- package/bin/mosetta.mjs +141 -0
- package/package.json +21 -3
- package/src/buildArchivePacker.mjs +144 -0
- package/src/buildConfigLoader.mjs +48 -0
- package/src/buildWorker.mjs +11 -0
- package/src/deploy.mjs +154 -0
- package/src/dev.mjs +145 -0
- package/src/devBuilder.mjs +116 -0
- package/src/devKeys.mjs +71 -0
- package/src/devStaticDir.mjs +112 -0
- package/src/engineLoader.mjs +46 -0
- package/src/evaluate.mjs +18 -0
- package/src/host/nodeFileSystem.mjs +45 -0
- package/src/log.mjs +114 -0
- package/src/login.mjs +136 -0
- package/src/push.mjs +99 -0
- package/src/scanner.mjs +26 -0
- package/src/serverPackage.mjs +75 -0
- package/src/staticBuild.mjs +100 -0
- package/src/typecheck.mjs +107 -0
- package/src/version.mjs +66 -0
- package/src/wasmEngine.mjs +55 -0
- package/src/workerd.mjs +172 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @mosetta/cli
|
|
2
|
+
|
|
3
|
+
## 0.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @mosetta/runtime@0.0.9
|
|
9
|
+
- @mosetta/build@0.0.9
|
|
10
|
+
- @mosetta/server@0.0.9
|
|
11
|
+
- @mosetta/stores@0.0.9
|
|
12
|
+
|
|
13
|
+
## 0.0.8
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- A project's own tile palette. The registry keeps the palettes a project's site takes tiles from;
|
|
18
|
+
the factory reads each one's tile host after the platform's, and takes a tile of a host other than
|
|
19
|
+
the primary only with its integrity. The rule a site's tile hosts merge by lives in
|
|
20
|
+
`@mosetta/core` (`mergeTileHosts`), so the editor offers the blocks of every tile host of the site
|
|
21
|
+
by the same rule (`TILE_HOSTS_TOKEN` in `@mosetta/server-action`). The platform's API publishes
|
|
22
|
+
into `palettes/<brand>` for the members of the project of that namespace, with the palette's stores
|
|
23
|
+
granted to that one request. `mosetta build --released <descriptor>` leaves out what a platform
|
|
24
|
+
already holds; `checkPaletteBuild` says what a palette may hold. The CLI's and the build's output
|
|
25
|
+
speaks the vocabulary: tile palettes, tiles, package tiles.
|
|
26
|
+
- Updated dependencies
|
|
27
|
+
- @mosetta/server@0.0.8
|
|
28
|
+
- @mosetta/build@0.0.8
|
|
29
|
+
- @mosetta/runtime@0.0.8
|
|
30
|
+
- @mosetta/stores@0.0.8
|
|
31
|
+
|
|
32
|
+
## 0.0.7
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- One vocabulary: code is tiles, content is blueprints, running requests is the factory, and a tile and a
|
|
37
|
+
blueprint made in the factory give a mosaic. A tile is a folder under `tiles/` (was `modules/`) whose
|
|
38
|
+
files are its sides, each defined with `define<Side>Side` — `defineComponentSide`, `defineEditorSide`,
|
|
39
|
+
`defineFallbackSide`, `defineRootComponentSide`, `defineServerSide`, `defineSsrSide` (were
|
|
40
|
+
`define…Module`); `useTile` (was `useModule`), `markInternalTile`, `BlueprintRoot`,
|
|
41
|
+
`BlueprintNodeRenderer`. `mosetta.json` names the tile `brand` (was `namespace`; a config that still
|
|
42
|
+
says `namespace` is refused with the word to use). The store connectors' package is `@mosetta/stores`
|
|
43
|
+
(was `@mosetta/provider`): `IStoreConnector`, `StoreConnectors`, `tileHostsOf`. The server's runner
|
|
44
|
+
is the factory gate (`@mosetta/server/factoryGate`, was `/runner`). Nothing on the wire moves: tile
|
|
45
|
+
ids keep `_ns_`, a build keeps `modules/<id>/`, stored JSON keeps its fields.
|
|
46
|
+
- Updated dependencies
|
|
47
|
+
- @mosetta/runtime@0.0.7
|
|
48
|
+
- @mosetta/server@0.0.7
|
|
49
|
+
- @mosetta/build@0.0.7
|
|
50
|
+
- @mosetta/stores@0.0.7
|
|
51
|
+
|
|
52
|
+
## 0.0.6
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- Updated dependencies
|
|
57
|
+
- @mosetta/provider@0.0.6
|
|
58
|
+
- @mosetta/server@0.0.6
|
|
59
|
+
- @mosetta/runtime@0.0.6
|
|
60
|
+
- @mosetta/build@0.0.6
|
|
61
|
+
|
|
62
|
+
## 0.0.5
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [92417dc]
|
|
67
|
+
- @mosetta/server@0.0.5
|
|
68
|
+
- @mosetta/build@0.0.5
|
|
69
|
+
- @mosetta/provider@0.0.5
|
|
70
|
+
- @mosetta/runtime@0.0.5
|
|
71
|
+
|
|
72
|
+
## 0.0.4
|
|
73
|
+
|
|
74
|
+
### Patch Changes
|
|
75
|
+
|
|
76
|
+
- 7577aa3: A server action asks the project's secrets by name — `SECRETS_TOKEN` (`get(name)` → the value or null) — and reads the
|
|
77
|
+
request it answers — `REQUEST_TOKEN` (its address and headers). The host grants each server module's isolate a
|
|
78
|
+
capability for its own scopes (`/`, `/<namespace>`, `/<namespace>/<module>`, the narrowest wins); no value sits in an
|
|
79
|
+
isolate's env, and pages get none. The registry keeps a project's secrets and plain settings, sealed with AES-GCM under
|
|
80
|
+
a root key the host provides. `mosetta dev` reads them from `.mosetta/secrets.json`.
|
|
81
|
+
- Updated dependencies [7577aa3]
|
|
82
|
+
- Updated dependencies [7577aa3]
|
|
83
|
+
- @mosetta/server@0.0.4
|
|
84
|
+
- @mosetta/build@0.0.4
|
|
85
|
+
- @mosetta/provider@0.0.4
|
|
86
|
+
- @mosetta/runtime@0.0.4
|
|
87
|
+
|
|
88
|
+
## 0.0.3
|
|
89
|
+
|
|
90
|
+
### Patch Changes
|
|
91
|
+
|
|
92
|
+
- a8d5a2c: A person acts on the platform from a program with a platform token: `mosetta login` signs the CLI in by a code the
|
|
93
|
+
person approves in the browser (a device flow), and keeps the token per installation in their config folder, readable
|
|
94
|
+
by them only; `MOSETTA_TOKEN` wins over it. The registry keeps tokens as hashes only, and the CLI's sign-in requests;
|
|
95
|
+
the cabinet's Worker makes, lists and revokes tokens and approves sign-ins.
|
|
96
|
+
- 1330d5d: Code is published THROUGH a platform: `mosetta push` describes the build (both catalogs, every file with its SHA-256,
|
|
97
|
+
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
|
|
98
|
+
publisher's side. The release rules check a fresh module's bytes against the catalog entry that vouches for them, on
|
|
99
|
+
either path. A runner can grant stores to server actions only (found by name), never read for its own modules — how
|
|
100
|
+
the platform's API writes code stores whose keys stay with its host; the host names the publish target in a header of
|
|
101
|
+
its own, beside the caller's identity.
|
|
102
|
+
- 886c43a: The release rules live in `@mosetta/build/release`, portable, for every publisher: `mosetta deploy` runs them against a
|
|
103
|
+
provider registry, a platform can run them on its side. A release writes each catalog over exactly the copy it planned
|
|
104
|
+
from and a module file only where none is, so two deploys into one store at once can no longer drop each other's
|
|
105
|
+
entries — the one overtaken plans again. A provider write takes a condition (`ifMatch`, `ifNoneMatch: '*'`), answered
|
|
106
|
+
412 when someone else got there first.
|
|
107
|
+
- Updated dependencies [1330d5d]
|
|
108
|
+
- Updated dependencies [886c43a]
|
|
109
|
+
- Updated dependencies [842344f]
|
|
110
|
+
- @mosetta/build@0.0.3
|
|
111
|
+
- @mosetta/provider@0.0.3
|
|
112
|
+
- @mosetta/server@0.0.3
|
|
113
|
+
- @mosetta/runtime@0.0.3
|
|
114
|
+
|
|
115
|
+
## 0.0.2
|
|
116
|
+
|
|
117
|
+
### Patch Changes
|
|
118
|
+
|
|
119
|
+
- The first versioned release. A deployed version never changes, and the stores keep every version
|
|
120
|
+
deployed. A page runs one copy of each framework package — the newest its blocks were built with,
|
|
121
|
+
or the version its root pins; the Framework distribution's updater lifts a page to its framework.
|
|
122
|
+
- Updated dependencies
|
|
123
|
+
- @mosetta/server@0.0.2
|
|
124
|
+
- @mosetta/build@0.0.2
|
|
125
|
+
- @mosetta/provider@0.0.2
|
|
126
|
+
- @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
|
-
#
|
|
1
|
+
# @mosetta/cli
|
|
2
2
|
|
|
3
|
-
|
|
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/`.
|
package/bin/mosetta.mjs
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.0.9",
|
|
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/build": "0.0.9",
|
|
17
|
+
"@mosetta/runtime": "0.0.9",
|
|
18
|
+
"@mosetta/server": "0.0.9",
|
|
19
|
+
"@mosetta/stores": "0.0.9"
|
|
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
|
+
}
|