@runeditrun/rer 0.1.1 → 0.2.1
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/README.md +113 -2
- package/cli/README.md +32 -5
- package/cli/contract.mjs +5 -5
- package/cli/deploy.mjs +17 -3
- package/cli/detached-server.mjs +51 -0
- package/cli/dev-identity.mjs +2 -1
- package/cli/door-mode.mjs +134 -0
- package/cli/door.mjs +263 -0
- package/cli/hosted-output.mjs +17 -7
- package/cli/hydration/browser.mjs +91 -0
- package/cli/hydration/cases.mjs +106 -0
- package/cli/hydration/check.mjs +105 -0
- package/cli/hydration/probe.mjs +164 -0
- package/cli/hydration/scripts.mjs +39 -0
- package/cli/hydration/verdict.mjs +85 -0
- package/cli/inspect.mjs +57 -10
- package/cli/lifecycle.mjs +36 -12
- package/cli/local.mjs +9 -3
- package/cli/main.mjs +3 -0
- package/cli/managed-server.mjs +62 -21
- package/cli/package-calls.mjs +81 -0
- package/cli/package-mode.mjs +46 -0
- package/cli/parse.mjs +15 -4
- package/cli/pnpm-runtime.mjs +96 -0
- package/cli/portability-worker.mjs +2 -2
- package/cli/release.mjs +1 -0
- package/cli/runtime-front-door.mjs +4 -2
- package/cli/server-proxy.mjs +69 -5
- package/cli/server-registry.mjs +10 -7
- package/cli/settings.mjs +14 -0
- package/cli/site-bindings.mjs +291 -0
- package/cli/site-browser-objects.mjs +36 -0
- package/cli/site-check.mjs +111 -0
- package/cli/site-client.mjs +50 -0
- package/cli/site-data.mjs +54 -0
- package/cli/site-entities.json +1 -0
- package/cli/site-input-assets.mjs +58 -0
- package/cli/site-inputs.mjs +79 -0
- package/cli/site-markup.mjs +102 -0
- package/cli/site-preview.mjs +53 -0
- package/cli/site-private.mjs +36 -0
- package/cli/site-references.mjs +93 -0
- package/cli/site-render.mjs +79 -0
- package/cli/site-syntax.mjs +21 -0
- package/cli/site.mjs +130 -0
- package/global/CHANGELOG.md +88 -0
- package/global/README.md +113 -2
- package/global/checkout.mjs +24 -10
- package/global/cli/install.mjs +6 -2
- package/global/delegate.mjs +9 -7
- package/global/door/command.mjs +254 -0
- package/global/door/control.mjs +65 -0
- package/global/door/cookies.mjs +41 -0
- package/global/door/pages.mjs +49 -0
- package/global/door/policy.mjs +49 -0
- package/global/door/routes.mjs +45 -0
- package/global/door/server.mjs +255 -0
- package/global/door/sites.mjs +192 -0
- package/global/launch/dev.mjs +21 -7
- package/global/launch/door.mjs +36 -0
- package/global/launch/install-pinned.mjs +22 -0
- package/global/launch/prepare.mjs +38 -5
- package/global/launch/render.mjs +6 -1
- package/global/launch/steps.mjs +12 -7
- package/global/launch/worker.mjs +3 -3
- package/global/list.mjs +6 -3
- package/global/main.mjs +39 -3
- package/global/output.mjs +12 -0
- package/global/rer.mjs +3 -0
- package/global/version.mjs +8 -8
- package/modules/dependencies/protocol.mjs +2 -1
- package/modules/deploy-kit/README.md +34 -1
- package/modules/deploy-kit/artifact.mjs +10 -0
- package/modules/deploy-kit/deploy.mjs +5 -2
- package/modules/deploy-kit/diagnostics.mjs +55 -0
- package/modules/deploy-kit/owner-cli.mjs +10 -4
- package/modules/deploy-kit/owner.mjs +8 -4
- package/modules/deploy-kit/prepare.mjs +3 -2
- package/modules/deploy-kit/provider.mjs +51 -9
- package/modules/deploy-kit/site.mjs +135 -0
- package/modules/erasure/CONTRACT.md +6 -3
- package/modules/erasure/README.md +8 -1
- package/modules/erasure/audit.mjs +2 -2
- package/modules/erasure/cascade.mjs +52 -11
- package/modules/erasure/order.mjs +29 -22
- package/modules/erasure/plan.mjs +4 -2
- package/modules/erasure/queue.mjs +41 -0
- package/modules/erasure/scope.mjs +3 -3
- package/modules/erasure/verification.mjs +4 -2
- package/modules/mail/README.md +1 -1
- package/modules/mail/index.mjs +1 -1
- package/modules/mail/src/core/errors.mjs +9 -0
- package/modules/mail/src/development/index.d.mts +6 -0
- package/modules/mail/src/development/index.mjs +6 -3
- package/modules/mail/src/development/viewer-html.mjs +3 -3
- package/modules/mail/src/development/viewer.mjs +8 -1
- package/modules/operator-auth/accounts/native.mjs +1 -1
- package/modules/operator-auth/dev/badge.mjs +4 -1
- package/modules/operator-auth/dev/index.mjs +15 -5
- package/modules/operator-auth/http.mjs +2 -3
- package/modules/operator-auth/index.d.mts +4 -9
- package/modules/operator-auth/loopback.mjs +17 -0
- package/modules/operator-auth/page.mjs +1 -1
- package/modules/operator-auth/service.mjs +1 -0
- package/modules/operator-auth/signin/index.d.mts +11 -0
- package/modules/operator-auth/signin/index.mjs +10 -7
- package/modules/owner-claim/verify.mjs +2 -1
- package/modules/portability/README.md +8 -0
- package/modules/portability/hash.mjs +6 -2
- package/modules/portability/import.mjs +1 -1
- package/modules/portability/legacy-files.mjs +4 -4
- package/modules/portability/legacy-safety.mjs +51 -10
- package/modules/portability/migration-history.mjs +15 -0
- package/modules/release/README.md +68 -3
- package/modules/release/keygen.mjs +18 -0
- package/modules/release/produce.mjs +14 -2
- package/modules/release/sign.mjs +44 -0
- package/modules/release/signature.mjs +336 -0
- package/modules/release/signing-cli.mjs +56 -0
- package/modules/release/signing-io.mjs +61 -0
- package/modules/release/signing-key.mjs +22 -0
- package/modules/release/source.mjs +15 -4
- package/modules/release/statement.mjs +80 -0
- package/modules/release/vectors/generate.mjs +381 -0
- package/modules/release/vectors/release-signature-vectors.json +2305 -0
- package/modules/release/verify.mjs +27 -0
- package/onboarding/adapter-auth.mjs +4 -3
- package/onboarding/adapter-settings.mjs +21 -5
- package/onboarding/adapter.mjs +2 -1
- package/onboarding/app-fetch.mjs +79 -0
- package/onboarding/archive-output.d.mts +4 -2
- package/onboarding/archive-output.mjs +22 -10
- package/onboarding/dependency-approval.mjs +4 -3
- package/onboarding/local/environment.mjs +7 -2
- package/onboarding/operator-install.mjs +8 -2
- package/onboarding/operator-prerequisites.mjs +5 -2
- package/onboarding/production-assets.mjs +23 -0
- package/onboarding/production-runtime.mjs +8 -4
- package/onboarding/queued-entry.mjs +9 -5
- package/operations/operations.mjs +11 -2
- package/package.json +76 -1
- package/scripts/publish-check/README.md +27 -17
- package/scripts/publish-check/content_identities.py +14 -233
- package/scripts/publish-check/git_history.py +2 -2
- package/scripts/publish-check/github_refs.py +28 -2
- package/scripts/publish-check/gitleaks.toml +1 -1
- package/scripts/publish-check/history_scan.py +6 -4
- package/scripts/publish-check/hygiene_config.py +3 -1
- package/scripts/publish-check/public_hygiene.py +63 -8
- package/scripts/publish-check/publication-config.json +16 -1
- package/scripts/publish-check/source_metadata.py +62 -0
- package/scripts/publish-check/source_shapes.py +91 -0
- package/scripts/publish-check/tree_scan.py +2 -2
- package/scripts/publish-check/source_hashes.mjs +0 -6
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { memberName, stringValue, walkSyntax } from './site-syntax.mjs';
|
|
2
|
+
import { localValues } from './site-bindings.mjs';
|
|
3
|
+
import { browserObjects } from './site-browser-objects.mjs';
|
|
4
|
+
import { markupReferences, cssReferences, markupStrings, cssStrings } from './site-markup.mjs';
|
|
5
|
+
|
|
6
|
+
export function javascriptContent(tree) {
|
|
7
|
+
const values = localValues(tree), {strings, names, resources, origins, global, localClass} = values;
|
|
8
|
+
const browser = browserObjects(values), found = [], literals = [], unresolved = [];
|
|
9
|
+
const properties = new Set(['src','href','srcset','action','formaction','poster','data']);
|
|
10
|
+
const elementFactory = callee => {
|
|
11
|
+
const methods = names(callee);
|
|
12
|
+
if (!methods.some(name => ['createElement','jsx','jsxs','jsxDEV'].includes(name)) || localClass(callee.object)) return false;
|
|
13
|
+
const receivers = origins(callee.object);
|
|
14
|
+
// Native document construction takes options, whereas framework factories
|
|
15
|
+
// render props. Mixed receiver alternatives must retain their framework use.
|
|
16
|
+
return !(methods.every(name => name === 'createElement') && receivers.length && receivers.every(browser.document));
|
|
17
|
+
};
|
|
18
|
+
const add = (node, kind, navigates = false, documentRelative = true, srcset = false) => {
|
|
19
|
+
const targets = resources(node);
|
|
20
|
+
if (targets.some(target => !target.complete)) unresolved.push({kind});
|
|
21
|
+
for (const {value, complete} of targets) for (const url of srcset ? value.split(',').map(part => part.trim().split(/\s+/)[0]) : [value]) found.push({url, navigates, documentRelative, dynamic:!complete});
|
|
22
|
+
};
|
|
23
|
+
const css = (node, emitted) => {for (const {value, complete} of resources(node)) {
|
|
24
|
+
literals.push(...cssStrings(value));
|
|
25
|
+
if (!emitted) continue;
|
|
26
|
+
const references = cssReferences(value);
|
|
27
|
+
found.push(...references.map(reference => ({...reference,documentRelative:true,dynamic:!complete})));
|
|
28
|
+
if (!complete && references.length) unresolved.push({kind:'CSS.url'});
|
|
29
|
+
}};
|
|
30
|
+
const markup = (node, emitted) => { for (const value of strings(node)) {
|
|
31
|
+
literals.push(...markupStrings(value));
|
|
32
|
+
if (emitted) found.push(...markupReferences(value, true).map(reference => ({...reference, documentRelative:true})));
|
|
33
|
+
}};
|
|
34
|
+
walkSyntax(tree, node => {
|
|
35
|
+
if (['ImportDeclaration','ExportNamedDeclaration','ExportAllDeclaration','ImportExpression'].includes(node.type) && node.source) add(node.source, node.type === 'ImportExpression' ? 'import()' : 'import', false, false);
|
|
36
|
+
if (node.type === 'AssignmentExpression') {
|
|
37
|
+
const object = node.left.object, fields = names(node.left);
|
|
38
|
+
if (browser.location(node.left)) add(node.right, 'location.href');
|
|
39
|
+
for (const property of fields) {
|
|
40
|
+
const name = property.toLowerCase();
|
|
41
|
+
if (properties.has(name) && browser.dom(object)) add(node.right, `DOM.${property}`, name === 'href' && browser.anchor(object), true, name === 'srcset');
|
|
42
|
+
if (property === 'href' && browser.location(object)) add(node.right, 'location.href');
|
|
43
|
+
if (['innerHTML','outerHTML','srcdoc'].includes(property)) markup(node.right, browser.dom(object));
|
|
44
|
+
}
|
|
45
|
+
// String extraction retains its privacy scope even for inert object fields.
|
|
46
|
+
if (fields.some(property => ['style','cssText'].includes(property)) || origins(object).some(value => memberName(value) === 'style')) css(node.right, browser.style(object) || (fields.includes('style') && browser.dom(object)));
|
|
47
|
+
}
|
|
48
|
+
if (node.type === 'Property') {
|
|
49
|
+
const property = node.computed ? stringValue(node.key) : node.key.name ?? node.key.value;
|
|
50
|
+
// Keep extracting private text from stored markup; resource use is checked
|
|
51
|
+
// where the value reaches a DOM write or a recognized element factory.
|
|
52
|
+
if (property === '__html') markup(node.value, false);
|
|
53
|
+
}
|
|
54
|
+
if (!['CallExpression','NewExpression'].includes(node.type)) return;
|
|
55
|
+
const args = node.arguments;
|
|
56
|
+
const createsElement = node.type === 'CallExpression' && origins(node.callee).some(elementFactory);
|
|
57
|
+
const tags = createsElement ? strings(args[0]) : [];
|
|
58
|
+
if (tags.length) for (const props of origins(args[1])) {
|
|
59
|
+
if (props.type !== 'ObjectExpression') continue;
|
|
60
|
+
for (const field of props.properties) for (const property of names(field)) {
|
|
61
|
+
if (properties.has(property.toLowerCase())) add(field.value, `DOM.${property}`, property === 'href' && tags.some(tag => ['a','area'].includes(tag)), true, property.toLowerCase() === 'srcset');
|
|
62
|
+
if (property === 'dangerouslySetInnerHTML') for (const html of origins(field.value)) {
|
|
63
|
+
if (html.type === 'ObjectExpression') for (const content of html.properties) if (names(content).includes('__html')) markup(content.value, true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const name of ['fetch','Worker','SharedWorker','WebSocket','EventSource']) if (global(node.callee, name)) add(args[0], name);
|
|
68
|
+
for (const callee of origins(node.callee)) for (const method of names(callee)) {
|
|
69
|
+
const object = callee.object;
|
|
70
|
+
if (method === 'sendBeacon' && global(object, 'navigator')) add(args[0], 'navigator.sendBeacon');
|
|
71
|
+
// URL and Request construct data. Their arguments are resolved only when
|
|
72
|
+
// the resulting value reaches a resource operation such as fetch.
|
|
73
|
+
if (method === 'setProperty' && origins(object).some(value => memberName(value) === 'style')) css(args[1], browser.style(object));
|
|
74
|
+
if (method === 'open') {
|
|
75
|
+
if (browser.window(object)) add(args[0], 'window.open');
|
|
76
|
+
else if (browser.constructed(object, 'XMLHttpRequest')) add(args[1], 'XMLHttpRequest.open');
|
|
77
|
+
}
|
|
78
|
+
if (['assign','replace'].includes(method) && browser.location(object)) add(args[0], `location.${method}`);
|
|
79
|
+
if (['setAttribute','setAttributeNS'].includes(method)) {
|
|
80
|
+
const index = method === 'setAttributeNS' ? 1 : 0;
|
|
81
|
+
for (const attribute of strings(args[index])) {
|
|
82
|
+
const property = attribute.toLowerCase();
|
|
83
|
+
if (properties.has(property) && browser.dom(object)) add(args[index + 1], `DOM.${method}(${property})`, property === 'href' && browser.anchor(object), true, property === 'srcset');
|
|
84
|
+
if (property === 'srcdoc') markup(args[index + 1], browser.dom(object));
|
|
85
|
+
if (property === 'style') css(args[index + 1], browser.dom(object));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (method === 'insertAdjacentHTML') markup(args[1], browser.dom(object));
|
|
89
|
+
if (method === 'write' || method === 'writeln') for (const argument of args) markup(argument, browser.document(object));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return {references:found, unresolved, literals};
|
|
93
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Child of `rer build --site` and `rer dev`: builds the seed's declared site
|
|
3
|
+
// entry with the checkout's own Vite for Node (an SSR build), loads it and
|
|
4
|
+
// calls its sitePages({ styles }), then writes the static run site: "/" to
|
|
5
|
+
// index.html, "/run" to run.html, "/docs/a" to docs/a.html, "/404" to
|
|
6
|
+
// 404.html, the declared public files, and the stylesheet and files the
|
|
7
|
+
// entry's CSS imports compile to. The optional site config is a Vite config
|
|
8
|
+
// naming the plugins the site pages need (a docs virtual module, a CSS
|
|
9
|
+
// toolchain); the app's own Vite config is never loaded, because its Worker
|
|
10
|
+
// plugins do not run in Node. It reads no installation data.
|
|
11
|
+
// Usage: node site-render.mjs ROOT ENTRY OUTPUT [CONFIG]
|
|
12
|
+
import { cp, lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { buildClients, writeSiteAsset } from './site-client.mjs';
|
|
17
|
+
|
|
18
|
+
const [root, entry, output, config, clientJSON] = process.argv.slice(2);
|
|
19
|
+
const fail = message => { process.stderr.write(`${message}\n`); process.exit(2); };
|
|
20
|
+
if (!root || !entry || !output) fail('Usage: site-render.mjs ROOT ENTRY OUTPUT [CONFIG]');
|
|
21
|
+
const inside = (base, path) => { const target = resolve(base, path); return target === base || target.startsWith(base + sep) ? target : null; };
|
|
22
|
+
// Compiled CSS and the files it references land under this folder of the site.
|
|
23
|
+
const ASSETS = 'site-assets';
|
|
24
|
+
|
|
25
|
+
let vite;
|
|
26
|
+
try { vite = await import(pathToFileURL(createRequire(join(root, 'package.json')).resolve('vite')).href); }
|
|
27
|
+
catch { fail('The run site renders with the checkout\'s own Vite. Run rer install first.'); }
|
|
28
|
+
process.env.NODE_ENV ??= 'production';
|
|
29
|
+
|
|
30
|
+
// The Node bundle sits beside the output, inside the checkout, so its external
|
|
31
|
+
// imports resolve from the checkout's node_modules. It never enters the site.
|
|
32
|
+
const server = `${output}.render`;
|
|
33
|
+
let module, styles = [], files = [];
|
|
34
|
+
try {
|
|
35
|
+
const built = await vite.build({
|
|
36
|
+
root, configFile: config ? resolve(root, config) : false, envFile: false, logLevel: 'error', mode: 'production',
|
|
37
|
+
build: {
|
|
38
|
+
ssr: resolve(root, entry), outDir: server, emptyOutDir: true, copyPublicDir: false, ssrEmitAssets: true,
|
|
39
|
+
cssCodeSplit: false, assetsDir: ASSETS, sourcemap: false, minify: false, reportCompressedSize: false, watch: null,
|
|
40
|
+
},
|
|
41
|
+
}).catch(error => fail(`The run site entry ${entry} did not build: ${error.message.split('\n')[0]}${config ? '' : ' If its pages need Vite plugins (a docs virtual module, a CSS toolchain), name a site Vite config: site: { config } in the command adapter.'}`));
|
|
42
|
+
const outputs = (Array.isArray(built) ? built : [built]).flatMap(result => result.output ?? []);
|
|
43
|
+
const chunk = outputs.find(item => item.type === 'chunk' && item.isEntry);
|
|
44
|
+
if (!chunk) fail(`The run site entry ${entry} produced no module.`);
|
|
45
|
+
files = outputs.filter(item => item.type === 'asset' && item.fileName.startsWith(`${ASSETS}/`)).map(item => item.fileName);
|
|
46
|
+
styles = files.filter(name => name.endsWith('.css')).map(name => `/${name}`);
|
|
47
|
+
await writeFile(join(server, 'package.json'), '{"type":"module"}\n');
|
|
48
|
+
module = await import(pathToFileURL(join(server, chunk.fileName)).href);
|
|
49
|
+
if (typeof module.sitePages !== 'function') fail(`${entry} must export sitePages(), returning { pages: [{ path, html }], assets: [public paths] }.`);
|
|
50
|
+
const clientBuild = await buildClients(vite, root, output, clientJSON ? JSON.parse(clientJSON) : {}), clients = clientBuild.clients;
|
|
51
|
+
styles = [...new Set([...styles,...clientBuild.styles])];
|
|
52
|
+
const build = await module.sitePages({ styles, clients });
|
|
53
|
+
if (!Array.isArray(build?.pages) || !Array.isArray(build.assets)) fail('sitePages() returns { pages, assets }.');
|
|
54
|
+
|
|
55
|
+
const written = new Set();
|
|
56
|
+
for (const page of build.pages) {
|
|
57
|
+
if (typeof page?.path !== 'string' || !/^\/(?:[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*)?$/i.test(page.path) || page.path.endsWith('.html') || typeof page.html !== 'string') fail(`Invalid site page ${JSON.stringify(page?.path)}: a path such as /run with its HTML.`);
|
|
58
|
+
const file = page.path === '/' ? 'index.html' : `${page.path.slice(1)}.html`;
|
|
59
|
+
if (written.has(file) || file.startsWith(`${ASSETS}/`)) fail(`Duplicate site page ${page.path}.`);
|
|
60
|
+
written.add(file);
|
|
61
|
+
await mkdir(dirname(join(output, file)), { recursive: true });
|
|
62
|
+
await writeFile(join(output, file), page.html);
|
|
63
|
+
}
|
|
64
|
+
const publicDirectory = resolve(root, 'public');
|
|
65
|
+
for (const asset of build.assets) {
|
|
66
|
+
const source = typeof asset === 'string' && !asset.split('/').some(part => !part || part.startsWith('.')) && inside(publicDirectory, asset);
|
|
67
|
+
if (!source || source === publicDirectory) fail(`Invalid site asset ${JSON.stringify(asset)}: a file or folder under public/.`);
|
|
68
|
+
if (asset === ASSETS || asset.startsWith(`${ASSETS}/`)) fail(`public/${ASSETS} is reserved for the run site's compiled styles; rename it.`);
|
|
69
|
+
await lstat(source).catch(() => fail(`Missing site asset public/${asset}.`));
|
|
70
|
+
await cp(source, join(output, asset), { recursive: true, errorOnExist: true, force: false, verbatimSymlinks: true });
|
|
71
|
+
}
|
|
72
|
+
// Only the emitted files: the Node chunks share the folder and never ship.
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
await writeSiteAsset(output,file,await readFile(join(server,file)));
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(`${JSON.stringify({ pages: build.pages.length, assets: build.assets.length, styles: styles.length })}\n`);
|
|
77
|
+
} finally { await rm(server, { recursive: true, force: true }); }
|
|
78
|
+
// Vite and the loaded pages can keep handles open; every file is written.
|
|
79
|
+
process.exit(0);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Shared ESTree helpers. Walk actual syntax so comments, string delimiters and
|
|
2
|
+
// formatting do not change which values the site checks.
|
|
3
|
+
export function walkSyntax(node, visit, parents = []) {
|
|
4
|
+
if (!node || typeof node !== 'object' || !node.type) return;
|
|
5
|
+
visit(node, parents);
|
|
6
|
+
for (const value of Object.values(node)) {
|
|
7
|
+
if (Array.isArray(value)) for (const child of value) walkSyntax(child, visit, [...parents, node]);
|
|
8
|
+
else if (value?.type) walkSyntax(value, visit, [...parents, node]);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export const memberName = node => node?.type === 'MemberExpression' ? node.computed ? stringValue(node.property) : node.property.name : undefined;
|
|
12
|
+
export const stringValue = node => node?.type === 'Literal' && typeof node.value === 'string' ? node.value : node?.type === 'TemplateLiteral' && !node.expressions.length ? node.quasis[0].value.cooked : undefined;
|
|
13
|
+
export function javascriptStrings(tree) {
|
|
14
|
+
const values = [];
|
|
15
|
+
walkSyntax(tree, node => {
|
|
16
|
+
const value = stringValue(node);
|
|
17
|
+
if (value !== undefined) values.push(value);
|
|
18
|
+
if (node.type === 'TemplateElement' && node.value.cooked !== null) values.push(node.value.cooked);
|
|
19
|
+
});
|
|
20
|
+
return values;
|
|
21
|
+
}
|
package/cli/site.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFile, rename, rm, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { contained } from './project.mjs';
|
|
5
|
+
import { invalid, missing, refuse, RerError } from './errors.mjs';
|
|
6
|
+
import { checkSite } from './site-check.mjs';
|
|
7
|
+
import { readDevKey } from './dev-identity.mjs';
|
|
8
|
+
import { shared } from './shared.mjs';
|
|
9
|
+
import { unpublishedIdentities } from './site-private.mjs';
|
|
10
|
+
import { privateSitePaths } from './site-inputs.mjs';
|
|
11
|
+
|
|
12
|
+
// The run site: a seed's static marketing pages, /run and docs, built from
|
|
13
|
+
// edition identity with no installation data. A seed opts in with
|
|
14
|
+
// commandAdapter's `site: { entry, output, declaration, config? }`: the module
|
|
15
|
+
// whose sitePages({ styles }) returns the pages, the ignored folder the build
|
|
16
|
+
// writes, the deploy declaration naming the run site's domain, and optionally
|
|
17
|
+
// the Vite config holding the plugins the site pages need.
|
|
18
|
+
const relativePath = value => typeof value === 'string' && /^[A-Za-z0-9._/-]+$/.test(value) && !value.split('/').some(part => !part || part === '.' || part === '..');
|
|
19
|
+
|
|
20
|
+
export function declaredSite(project) {
|
|
21
|
+
const site = project.adapter?.site;
|
|
22
|
+
if (!site) throw refuse('This checkout declares no run site. A seed opts in with site: { entry, output, declaration } in its command adapter; see SEED-COMMAND-CONTRACT.md.');
|
|
23
|
+
if (!relativePath(site.entry) || !relativePath(site.output) || !relativePath(site.declaration) || (site.config !== undefined && !relativePath(site.config)) || (site.clientConfig !== undefined && !relativePath(site.clientConfig)) || ['src', 'public', 'node_modules', 'migrations'].some(top => site.output === top || site.output.startsWith(top + '/'))) throw invalid('The declared run site needs relative entries/configs, an output folder outside src, public and node_modules, and a deploy declaration.');
|
|
24
|
+
if (site.clients !== undefined && (!site.clients || Array.isArray(site.clients) || typeof site.clients !== 'object' || Object.entries(site.clients).some(([name, entry]) => !/^[a-z][a-z0-9-]*$/i.test(name) || !relativePath(entry)))) throw invalid('Run site clients is a map of island names to relative client entry paths.');
|
|
25
|
+
return site;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Owner identity, secrets and the dev key: none may appear in the run site.
|
|
29
|
+
async function privateValues(project) {
|
|
30
|
+
const privateValues = [], privateIdentities = [], names = project.adapter.input?.names ?? {};
|
|
31
|
+
const { localVariables } = await shared('local/environment.mjs');
|
|
32
|
+
const manifest = await readFile(join(project.root, project.adapter.setup?.manifestFile ?? 'rer-setup.json'), 'utf8').then(JSON.parse, () => ({}));
|
|
33
|
+
const secretNames = new Set((manifest.fields ?? []).filter(field => field.kind === 'secret').map(field => field.name));
|
|
34
|
+
const variables = await readFile(join(project.root, project.adapter.hooks?.secrets?.file ?? '.dev.vars'), 'utf8').then(localVariables, () => ({}));
|
|
35
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
36
|
+
if ([names.email, names.name].includes(key)) privateIdentities.push(value);
|
|
37
|
+
else if (secretNames.has(key) || /SECRET|TOKEN|PASSWORD|_KEY$/.test(key)) privateValues.push(value);
|
|
38
|
+
}
|
|
39
|
+
const answers = await readFile(join(project.root, '.seed/installer/setup-answers.json'), 'utf8').then(JSON.parse, () => ({}));
|
|
40
|
+
for (const [key, value] of Object.entries(answers)) if (secretNames.has(key) || /SECRET|TOKEN|PASSWORD|_KEY$/.test(key)) privateValues.push(value);
|
|
41
|
+
for (const key of [names.email, names.name]) if (typeof answers?.[key] === 'string') privateIdentities.push(answers[key]);
|
|
42
|
+
const key = await readDevKey(project.root).catch(() => null);
|
|
43
|
+
if (key) privateValues.push(key);
|
|
44
|
+
return { privateValues, privateIdentities: await unpublishedIdentities(project.root, privateIdentities, privateSitePaths(project.adapter)) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Render the run site into its declared folder, check it and replace the previous build. */
|
|
48
|
+
export async function buildSite(project, store, signal) {
|
|
49
|
+
const site = declaredSite(project);
|
|
50
|
+
await contained(project.root, site.entry);
|
|
51
|
+
if (site.config) await contained(project.root, site.config);
|
|
52
|
+
if (site.clientConfig) await contained(project.root, site.clientConfig);
|
|
53
|
+
for (const entry of Object.values(site.clients ?? {})) await contained(project.root, entry);
|
|
54
|
+
const folder = await contained(project.root, site.output, { optional: true });
|
|
55
|
+
await mkdir(dirname(folder), { recursive: true });
|
|
56
|
+
const staging = `${folder}.building-${process.pid}`;
|
|
57
|
+
await rm(staging, { recursive: true, force: true });
|
|
58
|
+
try {
|
|
59
|
+
await mkdir(staging);
|
|
60
|
+
try { await store.run('Building the run site', process.execPath, [fileURLToPath(new URL('./site-render.mjs', import.meta.url)), project.root, site.entry, staging, site.config ?? '', JSON.stringify({entries:site.clients ?? {}, config:site.clientConfig, privatePaths:privateSitePaths(project.adapter)})], { cwd: project.root, signal, heavy: false }); }
|
|
61
|
+
catch (error) {
|
|
62
|
+
const code = await readFile(`${staging}.refusal`, 'utf8').catch(() => '');
|
|
63
|
+
if (code === 'site_private_input') throw new RerError(code, 'Run site client imports a private installation file or a file outside the seed and its declared dependencies.', 4);
|
|
64
|
+
if (code === 'site_client_server_import') throw new RerError(code, 'Run site client imports a server-only module. Use browser dependencies in every reachable island module.', 4);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const checked = await checkSite(staging, {root:project.root, ...await privateValues(project)});
|
|
68
|
+
await rm(folder, { recursive: true, force: true });
|
|
69
|
+
await rename(staging, folder);
|
|
70
|
+
return { folder, ...checked };
|
|
71
|
+
} finally { await Promise.all([staging, `${staging}.render`, `${staging}.refusal`].map(path => rm(path, { recursive: true, force: true }))); }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function siteDeployment(project, options, folder) {
|
|
75
|
+
const site = declaredSite(project), environment = options.environment ?? 'production';
|
|
76
|
+
const read = async path => { try { return JSON.parse(await readFile(await contained(project.root, path), 'utf8')); } catch (error) { if (error instanceof RerError) throw error; throw invalid(`Malformed ${path}.`); } };
|
|
77
|
+
const app = await read(project.adapter.deploy?.declaration ?? 'deploy.json'), declaration = await read(site.declaration);
|
|
78
|
+
const { sitePlan } = await import('../modules/deploy-kit/site.mjs');
|
|
79
|
+
try { return sitePlan({ project: app.project, environment, compatibilityDate: app.compatibilityDate, declaration, assets: folder }); }
|
|
80
|
+
catch (error) { throw invalid(`The run site deploy declaration is invalid (${error.message}). ${site.declaration} holds {"version": 1, "domain": "<apex hostname>"}.`); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `rer build --site` and `rer deploy --site [--dry-run]`. */
|
|
84
|
+
export async function siteCommand(project, options, store, signal, out) {
|
|
85
|
+
const built = await buildSite(project, store, signal);
|
|
86
|
+
if (!options.json && built.references.unresolved.length) {
|
|
87
|
+
out.human(`Run site references: ${built.references.unresolved.length} runtime resource target(s) could not be resolved statically:`);
|
|
88
|
+
for (const {asset, kind} of built.references.unresolved) out.human(` ${asset}: ${kind}`);
|
|
89
|
+
}
|
|
90
|
+
if (options.verb === 'build') {
|
|
91
|
+
// Output redaction masks an inherited PWD (a credential-shaped name), so the
|
|
92
|
+
// checkout-relative folder is named too.
|
|
93
|
+
const path = relative(project.root, built.folder);
|
|
94
|
+
if (!options.json) out.human(`Run site written to ${path} (${built.pages} pages, ${built.files} files): ${built.folder}`);
|
|
95
|
+
return { site: { folder: built.folder, path, pages: built.pages, files: built.files, references:built.references } };
|
|
96
|
+
}
|
|
97
|
+
const plan = await siteDeployment(project, options, built.folder);
|
|
98
|
+
const human = [`Run site plan: site Worker ${plan.worker}, Base-generated, its only binding the assets (the app's Worker ${plan.appWorker} is not touched)`, ` assets: ${plan.assets} (${built.files} files; HTML ${plan.assetsConfig.html_handling}, not found ${plan.assetsConfig.not_found_handling}; the Worker runs first)`,
|
|
99
|
+
` custom domains: ${plan.hostnames.join(', ')}; workers.dev off`, ...plan.redirects.map(rule => ` redirect in the Worker: ${rule.from} -> ${rule.to} (${rule.status}, keeps ${rule.keeps.join(' and ')})`)];
|
|
100
|
+
const summary = { worker: plan.worker, appWorker: plan.appWorker, module: plan.module, bindings: plan.bindings, assets: plan.assets, files: built.files, references:built.references, domain: plan.domain, hostnames: plan.hostnames, workersDev: false, runWorkerFirst: true, assetsConfig: plan.assetsConfig, redirects: plan.redirects.map(({ from, to, status }) => ({ from, to, status })) };
|
|
101
|
+
if (options.dryRun) {
|
|
102
|
+
if (options.json) out.event('site', summary); else for (const line of human) out.human(line);
|
|
103
|
+
store.report('deploy --site: dry run complete; no deployment occurred and no provider request was made.');
|
|
104
|
+
return { dryRun: true, deployed: false, site: summary };
|
|
105
|
+
}
|
|
106
|
+
const names = ['CLOUDFLARE_ACCOUNT_ID', 'CLOUDFLARE_API_TOKEN'];
|
|
107
|
+
if (names.some(name => !process.env[name]?.trim())) throw missing('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required, with the run site token permissions in the deploy-kit README.');
|
|
108
|
+
if (!options.json) for (const line of human) out.human(line);
|
|
109
|
+
const { cloudflare } = await import('../modules/deploy-kit/provider.mjs');
|
|
110
|
+
const { collectFiles } = await import('../modules/deploy-kit/artifact.mjs');
|
|
111
|
+
const { siteDeploy } = await import('../modules/deploy-kit/site.mjs');
|
|
112
|
+
const environment = options.environment ?? 'production';
|
|
113
|
+
try {
|
|
114
|
+
const api = cloudflare({ accountId: process.env.CLOUDFLARE_ACCOUNT_ID, token: process.env.CLOUDFLARE_API_TOKEN });
|
|
115
|
+
// The receipt is for the person; ownership is read from the provider, so a fresh checkout deploys too.
|
|
116
|
+
const result = await siteDeploy({ api, plan, files: await collectFiles(built.folder), receiptPath: join(project.root, '.rer', 'deploy', `site-${environment}.json`) });
|
|
117
|
+
return { dryRun: false, deployed: true, origin: result.origin, site: summary };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof RerError) throw error;
|
|
120
|
+
throw new RerError('deploy_refused', `Run site deploy stopped: ${/^[a-z0-9-]{1,80}$/.test(error.message) ? error.message : 'site-deploy-failed'}${error.resource ? ` (${error.resource})` : ''}.`, 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** For rer dev: build the run site and serve it on the first free port above the app's. */
|
|
125
|
+
export async function devSite(project, store, signal, { host, port }) {
|
|
126
|
+
const built = await buildSite(project, store, signal);
|
|
127
|
+
const { sitePreview } = await import('./site-preview.mjs');
|
|
128
|
+
return { ...await sitePreview(built.folder, { host, ports: Array.from({ length: 50 }, (_, index) => port + 1 + index).filter(value => value <= 65535) }), folder: relative(project.root, built.folder) };
|
|
129
|
+
}
|
|
130
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- `rer release keygen` prepares publisher keys and anchor entries; `rer release sign` writes a detached signature for a completed tagged release.
|
|
6
|
+
- `rer release receipt` verifies signed artifacts against local anchors offline, using the shared verifier and vectors exported for reuse.
|
|
7
|
+
- Tagged releases record the annotated tag object, public commit and public tree needed to sign their artifacts.
|
|
8
|
+
- Package-mode conversion removes obsolete vendored-copy checks, component selections and packaging globs, and rewrites test launches to the installed rer entry with foreground development.
|
|
9
|
+
- Hosted-output and hydration checks run when invoked through pnpm's package symlinks.
|
|
10
|
+
- Run sites support seed-declared browser client islands, with bundled scripts and styles checked during the build.
|
|
11
|
+
- Run-site privacy checks match complete owner identifiers and accept examples already committed in source; `rer dev` warns on a run-site privacy refusal while `rer build --site` still refuses.
|
|
12
|
+
- `rer preview --detach` starts a background preview, refreshes stale build output after source changes and serves scheduled trigger routes.
|
|
13
|
+
- `rer dev --foreground --help` shows the command's help.
|
|
14
|
+
- Deploy failures name the failed step and error code, show redacted provider output and list resources created during that attempt.
|
|
15
|
+
- Sign-in pages hide the email form when mail is unavailable and expose typed handler options.
|
|
16
|
+
- Dev badge and sign-in outbox controls have larger click targets.
|
|
17
|
+
- The Lobby stays script-free; `RER_OPEN` exchanges the development key on `/__dev/key` under its own content security policy.
|
|
18
|
+
- `rer --version` handles a closed output pipe without an EPIPE stack.
|
|
19
|
+
- The publication scan accepts recognised vendor no-reply co-author trailers while continuing to flag personal email addresses.
|
|
20
|
+
- Local-run instructions give the stop, export, reset, import and setup order.
|
|
21
|
+
- Base provides an optional run-site workflow template for manual adoption.
|
|
22
|
+
- Base's source and reference import kit can erase selected personal fields from retained shared records and undo images; this kit is outside the rer npm package.
|
|
23
|
+
|
|
24
|
+
### Known limitations
|
|
25
|
+
|
|
26
|
+
the run-site privacy check does not detect deliberately obfuscated secrets, such as base64 concatenated to other text, in client islands
|
|
27
|
+
|
|
28
|
+
## 0.2.0
|
|
29
|
+
|
|
30
|
+
What changes for a user since 0.1.1.
|
|
31
|
+
|
|
32
|
+
### Package mode
|
|
33
|
+
|
|
34
|
+
- A first-party seed pins `@runeditrun/rer` as an exact devDependency instead of
|
|
35
|
+
carrying `scripts/rer.mjs`, `scripts/base-cli` and `scripts/base-onboarding`.
|
|
36
|
+
`node scripts/export-cli.mjs --to-package SEED_ROOT [--version X]` converts a
|
|
37
|
+
vendored seed (it refuses local edits and lists them).
|
|
38
|
+
- `rer check` and `rer describe` in a checkout whose `node_modules` lacks its
|
|
39
|
+
pin install it with the frozen lockfile first, then run the pinned verb, so a
|
|
40
|
+
fresh clone checks with the pinned bytes. `rer check --profile first-party`
|
|
41
|
+
accepts package mode (SC-13) and still accepts vendored mode.
|
|
42
|
+
- Named exports for every Base file a seed's adapter, Vite config, build or
|
|
43
|
+
package script imports; nothing needs `internal/`.
|
|
44
|
+
|
|
45
|
+
### Lobby, homepage variants, setup state
|
|
46
|
+
|
|
47
|
+
- The Lobby (the pre-claim page) and `GET /setup/state`; a seed supplies its own
|
|
48
|
+
first-run read, and the fonts stylesheet its edition names.
|
|
49
|
+
- Homepage variants and a hydration check that follows a redirecting `/`, counts
|
|
50
|
+
cross-origin client scripts and fails an edit case React hydrated too early to test.
|
|
51
|
+
|
|
52
|
+
### Run sites
|
|
53
|
+
|
|
54
|
+
- `rer build --site` builds the seed's run site (with its own Vite config and
|
|
55
|
+
CSS) and `rer deploy --site` deploys it. Ownership is proven from the provider
|
|
56
|
+
alone (the latest deployment serves the newest version, which carries the
|
|
57
|
+
kit's annotation), so a fresh CI checkout redeploys; a foreign Worker or
|
|
58
|
+
domain is refused.
|
|
59
|
+
- The site Worker is Base-generated: `www.<domain>` answers a 301 to the apex
|
|
60
|
+
(path and query kept) and everything else comes from assets. No zone ruleset
|
|
61
|
+
is read or written.
|
|
62
|
+
- The publication scan accepts the run-NAME repository names the owner
|
|
63
|
+
proposed (for example run-lamp under the just-every account) as intended
|
|
64
|
+
public destinations; the earlier names stay valid.
|
|
65
|
+
|
|
66
|
+
### Local development hardening
|
|
67
|
+
|
|
68
|
+
- A request with a foreign `Host` gets a plain 421; keyless `/@fs` and any
|
|
69
|
+
path Vite would answer with its 403 "Restricted" page (which lists checkout
|
|
70
|
+
and home paths) get 404. The dev key opens them.
|
|
71
|
+
- `rer` fetches the pinned pnpm from the npm registry, checked against its
|
|
72
|
+
integrity hash, when PATH lacks it.
|
|
73
|
+
- An explicit `RER_SERVER_LIMIT` caps its own start, higher or lower.
|
|
74
|
+
- `rer door`: keys never go through the door, a moved checkout keeps its name,
|
|
75
|
+
a restart hands starts over, a missing folder is a plain page.
|
|
76
|
+
|
|
77
|
+
### Hosted output
|
|
78
|
+
|
|
79
|
+
- Built `.html` files are served by the hosted Worker (`staticAssetResponse`),
|
|
80
|
+
including revalidation (`If-None-Match` answers 304). The hosted check
|
|
81
|
+
requests every built file and sends one conditional request per `.html`.
|
|
82
|
+
- A deploy secret may carry `verifyStandIn` for the hosted check.
|
|
83
|
+
- Packed deploy assets leave out `.assetsignore` and what it names.
|
|
84
|
+
|
|
85
|
+
### Fixes
|
|
86
|
+
|
|
87
|
+
- Setup reads the seed's declared settings format and session-secret name.
|
|
88
|
+
- Large stores export, and a fresh install recovers a legacy backup.
|
package/global/README.md
CHANGED
|
@@ -97,6 +97,10 @@ RER_OPEN http://127.0.0.1:8787/__dev/key#...
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
or `RER_SIGN_IN <outbox>` for a seed that signs in through its development outbox.
|
|
100
|
+
A seed that declares a run site prints one more line after those,
|
|
101
|
+
`RER_SITE http://127.0.0.1:8788`: a local preview of its static run site (its
|
|
102
|
+
marketing homepage, `/run` and docs) on the first free port above the app's. It
|
|
103
|
+
stops with the app.
|
|
100
104
|
It never prompts and never re-runs setup on a checkout that is already set up. A
|
|
101
105
|
setup a launch started that was stopped or failed is run again by the next
|
|
102
106
|
`rer dev`, keeping the owner and origin it saved, unless a setup has finished
|
|
@@ -130,6 +134,27 @@ checkout never records a limit you did not set.
|
|
|
130
134
|
A start is never refused for machine load alone unless `RER_SERVERS_LOAD_THRESHOLD`
|
|
131
135
|
or `RER_JOBS_LOAD_THRESHOLD` is set; memory and disk guards stay.
|
|
132
136
|
|
|
137
|
+
## The front door
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
rer door enable # opt this computer in: port 8700, 10 servers, 12 hours idle
|
|
141
|
+
rer door add ~/review/* # register checkouts; prints each http://NAME.localhost:8700
|
|
142
|
+
rer door status --links # every checkout, its state, and sign-in links
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
One process on one port serves every registered checkout at
|
|
146
|
+
`http://NAME.localhost:PORT` (NAME is its folder name, unique and stable);
|
|
147
|
+
`http://localhost:PORT` lists them. A stopped checkout starts when you open it
|
|
148
|
+
in a browser and stops when idle; subresources, API calls and WebSockets never
|
|
149
|
+
start one. `rer door enable` stores the policy (`--port`, `--server-limit`,
|
|
150
|
+
`--idle-timeout`) and the `RER_*` variables in effect, so whoever later starts
|
|
151
|
+
the door cannot change what its checkouts run with. While it is enabled, `rer
|
|
152
|
+
dev` sets fresh checkouts up behind it; `rer dev --door` moves an existing one
|
|
153
|
+
(keeping its data), `rer dev --direct` moves it back, and `--port N` always
|
|
154
|
+
serves directly. `rer door start`, `stop`, `restart`, `disable` and `remove
|
|
155
|
+
NAME` do what they say; after a reboot or logout the door needs `rer door
|
|
156
|
+
start`. `rer door --help` has the details.
|
|
157
|
+
|
|
133
158
|
## Find what to launch
|
|
134
159
|
|
|
135
160
|
```sh
|
|
@@ -144,15 +169,101 @@ Independent projects (an `OPERATIONS.json` version 2 without a seed contract) ru
|
|
|
144
169
|
their declared commands with `rer <verb> [-- args]`; zero-setup launching does not
|
|
145
170
|
apply to them. `OPERATIONS.json` version 1 is not supported.
|
|
146
171
|
|
|
172
|
+
## Package mode: a checkout that pins `@runeditrun/rer`
|
|
173
|
+
|
|
174
|
+
A checkout runs one of two copies of rer. In **vendored mode** it carries
|
|
175
|
+
`scripts/rer.mjs`, `scripts/base-cli` and `scripts/base-onboarding`, and `rer`
|
|
176
|
+
runs that copy. In **package mode** it carries none of them and pins this package
|
|
177
|
+
as an exact devDependency instead (`"@runeditrun/rer": "0.2.0"`, locked in
|
|
178
|
+
`pnpm-lock.yaml`): its `OPERATIONS.json` argv and package scripts are `rer VERB`,
|
|
179
|
+
which inside `pnpm run` is the pinned copy in `node_modules/.bin`, so
|
|
180
|
+
`pnpm install --frozen-lockfile && pnpm run build` needs no global `rer`. A global
|
|
181
|
+
`rer` hands every verb to the pinned package, launching included. Until
|
|
182
|
+
`node_modules` holds the pinned version, `rer install` and `rer dev` install it
|
|
183
|
+
first (the frozen `pnpm install`) and every other verb says to run `rer install`.
|
|
184
|
+
|
|
185
|
+
Heavy package scripts wait for this computer's job queue with
|
|
186
|
+
`rer queue NAME -- COMMAND [ARGS]` (in vendored mode,
|
|
187
|
+
`node scripts/base-onboarding/queued-entry.mjs NAME -- COMMAND`).
|
|
188
|
+
|
|
189
|
+
`rer check --profile first-party` accepts either mode. In package mode it asks for
|
|
190
|
+
the exact devDependency (`SC-13.package.pin`), its lockfile entry
|
|
191
|
+
(`SC-13.package.lockfile`), no vendored copy (`SC-13.package.vendored.*`) and `rer
|
|
192
|
+
VERB` targets and aliases (`SC-02.target.*`, `SC-02.alias.*`); in vendored mode for
|
|
193
|
+
the copy and its recorded inventory, as before.
|
|
194
|
+
|
|
195
|
+
Convert a vendored seed from a Base checkout with
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
node scripts/export-cli.mjs --to-package SEED_ROOT [--version X]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
(the default version is the latest published one, read with the seed's own npm
|
|
202
|
+
configuration). It refuses, listing them, when the vendored copy differs from its
|
|
203
|
+
recorded inventory: carry those edits with `pnpm patch @runeditrun/rer` once
|
|
204
|
+
converted, or upstream them to Base. Otherwise it pins and locks the package
|
|
205
|
+
(`pnpm install --lockfile-only`), rewrites imports into the copy to the exports
|
|
206
|
+
below (anything else to `@runeditrun/rer/internal/PATH`, which promises nothing
|
|
207
|
+
beyond the pinned version), rewrites the package scripts and `OPERATIONS.json`
|
|
208
|
+
argv to `rer VERB` and the delivery operations (`rer-project.json` and the
|
|
209
|
+
`scripts/delivery/base/packaging.json` it is generated from), the shell scripts
|
|
210
|
+
they and CI workflows run, and workflow lines to `pnpm exec rer VERB` (a frozen
|
|
211
|
+
`pnpm install` first where nothing has installed yet), drops the copy's own records from `base-copies.json` (the
|
|
212
|
+
app-runtime copies it records stay as they are), removes the copy and lists every
|
|
213
|
+
line that still names it. A later refresh is a version bump:
|
|
214
|
+
`pnpm add -D -E @runeditrun/rer@X`.
|
|
215
|
+
|
|
216
|
+
### Exports
|
|
217
|
+
|
|
218
|
+
| Import | What it gives | Vendored path it replaces |
|
|
219
|
+
| --- | --- | --- |
|
|
220
|
+
| `@runeditrun/rer/adapter` | `commandAdapter` | `scripts/base-cli/cli/adapter.mjs` |
|
|
221
|
+
| `@runeditrun/rer/deploy` | `ownerDeploy` | `scripts/base-cli/cli/deploy.mjs` |
|
|
222
|
+
| `@runeditrun/rer/portability` | `localPortability` | `scripts/base-cli/cli/portability.mjs` |
|
|
223
|
+
| `@runeditrun/rer/errors` | the CLI's error helpers | `scripts/base-cli/cli/errors.mjs` |
|
|
224
|
+
| `@runeditrun/rer/jsonc` | JSONC reading | `scripts/base-cli/cli/jsonc.mjs` |
|
|
225
|
+
| `@runeditrun/rer/parse`, `/scheduled` | CLI argument parsing, scheduled-job helpers | `scripts/base-cli/cli/parse.mjs`, `scheduled.mjs` |
|
|
226
|
+
| `@runeditrun/rer/dev-identity` | dev identity helpers | `scripts/base-cli/cli/dev-identity.mjs` |
|
|
227
|
+
| `@runeditrun/rer/operator-installer` | `operatorInstaller`, `developmentTarget`, `previewTarget` | `scripts/base-onboarding/operator-installer.mjs` |
|
|
228
|
+
| `@runeditrun/rer/local-installer` | `localInstaller` | `scripts/base-onboarding/adapter.mjs` |
|
|
229
|
+
| `@runeditrun/rer/operator-install` | `installOperator` | `scripts/base-onboarding/operator-install.mjs` |
|
|
230
|
+
| `@runeditrun/rer/operator-prerequisites` | `operatorPrerequisites` | `scripts/base-onboarding/operator-prerequisites.mjs` |
|
|
231
|
+
| `@runeditrun/rer/local/state` | `selectLocalState` | `scripts/base-onboarding/local/state.mjs` |
|
|
232
|
+
| `@runeditrun/rer/local/build` | local build step | `scripts/base-onboarding/local/build.mjs` |
|
|
233
|
+
| `@runeditrun/rer/state`, `/commands` | onboarding state and command runners | `scripts/base-onboarding/state.mjs`, `commands.mjs` |
|
|
234
|
+
| `@runeditrun/rer/local/environment`, `/local/files`, `/local/operation`, `/local/recovery` | local setup helpers | `scripts/base-onboarding/local/*.mjs` |
|
|
235
|
+
| `@runeditrun/rer/private-files` | private file helpers | `scripts/base-onboarding/private-files.mjs` |
|
|
236
|
+
| `@runeditrun/rer/adapter-settings` | adapter settings helpers | `scripts/base-onboarding/adapter-settings.mjs` |
|
|
237
|
+
| `@runeditrun/rer/adapter-auth`, `/adapter-hosting` | adapter auth and hosting helpers | `scripts/base-onboarding/adapter-auth.mjs`, `adapter-hosting.mjs` |
|
|
238
|
+
| `@runeditrun/rer/process-diagnostics` | process diagnostics | `scripts/base-onboarding/process-diagnostics.mjs` |
|
|
239
|
+
| `@runeditrun/rer/process-cleanup`, `/process-scope` | process cleanup and scope | `scripts/base-onboarding/process-cleanup.mjs`, `process-scope.mjs` |
|
|
240
|
+
| `@runeditrun/rer/failures`, `/redaction` | failure and redaction helpers | `scripts/base-onboarding/failures.mjs`, `redaction.mjs` |
|
|
241
|
+
| `@runeditrun/rer/setup-manifest`, `/setup-schema` | setup manifest loading and its JSON schema | `scripts/base-onboarding/setup-manifest.mjs`, `schema/SETUP.schema.json` |
|
|
242
|
+
| `@runeditrun/rer/production-runtime`, `/production-modules` | production runtime helpers | `scripts/base-onboarding/production-runtime.mjs`, `production-modules.mjs` |
|
|
243
|
+
| `@runeditrun/rer/queued-entry` | the job-queue entry | `scripts/base-onboarding/queued-entry.mjs` |
|
|
244
|
+
| `@runeditrun/rer/archive-output` | `archiveOutputGuard` (with types) | `scripts/base-onboarding/archive-output.mjs` |
|
|
245
|
+
| `@runeditrun/rer/launcher` | the browser installer launcher | `scripts/base-onboarding/launcher.mjs` |
|
|
246
|
+
| `@runeditrun/rer/installer-server` | the browser installer's server (installer journeys) | `scripts/base-onboarding/server.mjs` |
|
|
247
|
+
| `@runeditrun/rer/server-resources` | managed server resource readings (runtime journeys) | `scripts/base-cli/cli/server-resources.mjs` |
|
|
248
|
+
| `@runeditrun/rer/operations` | OPERATIONS describe and check | `scripts/base-cli/operations/operations.mjs` |
|
|
249
|
+
| `@runeditrun/rer/modules/portability`, `/catalogue`, `/cli`, `/cli-options`, `/files`, `/format`, `/http`, `/import`, `/journal`, `/legacy-safety`, `/migration-history`, `/storage` | portability module | `scripts/base-cli/modules/portability/*.mjs` |
|
|
250
|
+
| `@runeditrun/rer/modules/release/export`, `/publication-scan`, `/source`, `/signature` | release export, publication scan and shared signature verifier | `scripts/base-cli/modules/release/*.mjs` |
|
|
251
|
+
| `@runeditrun/rer/modules/deploy-kit/owner`, `/owner-cli`, `/prepare`, `/provider` | deploy kit | `scripts/base-cli/modules/deploy-kit/*.mjs` |
|
|
252
|
+
| `@runeditrun/rer/modules/operator-auth`, `/dev`, `/dev/badge`, `/dev-admission`, `/accounts/native`, `/identity/email-link` | operator auth | `scripts/base-cli/modules/operator-auth/index.mjs`, `dev/index.mjs`, `dev/badge.mjs`, `dev-admission.mjs`, `accounts/native.mjs`, `identity/email-link/index.mjs` |
|
|
253
|
+
| `@runeditrun/rer/modules/erasure/fence` | erasure fence statements | `scripts/base-cli/modules/erasure/fence.mjs` |
|
|
254
|
+
| `@runeditrun/rer/modules/owner-claim`, `/modules/dependencies` | owner claim, dependency links | `scripts/base-cli/modules/owner-claim/index.mjs`, `dependencies/index.mjs` |
|
|
255
|
+
| `@runeditrun/rer/internal/PATH` | any other package file, for tests | `scripts/base-cli/PATH`, and `scripts/base-onboarding/PATH` with PATH under `onboarding/` |
|
|
256
|
+
|
|
147
257
|
## Verbs
|
|
148
258
|
|
|
149
259
|
In a checkout, every verb runs that checkout's own pinned `rer`; `rer VERB --help`
|
|
150
260
|
there lists its flags:
|
|
151
261
|
|
|
152
262
|
`install`, `setup`, `dev`, `preview`, `build`, `verify`, `reset --yes`, `deploy`,
|
|
153
|
-
`export`, `import`, `outbox`, `doctor`, `describe`, `check`, `release
|
|
263
|
+
`export`, `import`, `outbox`, `doctor`, `describe`, `check`, `release`,
|
|
264
|
+
`release sign`, `release receipt` and `release keygen`.
|
|
154
265
|
|
|
155
266
|
Anywhere: `rer list [DIR]`, `rer cli status|install|uninstall`, `rer update`,
|
|
156
|
-
`rer --version` and `rer --help`.
|
|
267
|
+
`rer queue NAME -- COMMAND`, `rer --version` and `rer --help`.
|
|
157
268
|
|
|
158
269
|
Licence: MIT.
|
package/global/checkout.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// above the working directory, or exactly --cwd DIR. Then pick whose code runs it.
|
|
3
3
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { exactVersion } from '../cli/package-mode.mjs';
|
|
5
6
|
import { ownRoot, packageName } from './package-info.mjs';
|
|
6
7
|
|
|
7
8
|
// The value of --cwd among the wrapper flags (never after a -- separator).
|
|
@@ -53,19 +54,32 @@ export function unsupportedMessage(root, version) {
|
|
|
53
54
|
return `${join(root, 'OPERATIONS.json')} is an OPERATIONS version ${version ?? '(none)'} descriptor, which rer does not run. Use the commands that project documents, or move it to OPERATIONS version 2.`;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// The rer package a checkout pins in package.json (package mode): the declared
|
|
58
|
+
// specifier, the version node_modules holds, and whether that installed copy is
|
|
59
|
+
// the one to run (`ready`: present, and equal to an exact pin). undefined for a
|
|
60
|
+
// checkout that does not declare it.
|
|
61
|
+
export function pinnedPackage(root) {
|
|
62
|
+
let manifest;
|
|
63
|
+
try { manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); } catch { return undefined; }
|
|
64
|
+
const declared = manifest.devDependencies?.[packageName] ?? manifest.dependencies?.[packageName];
|
|
65
|
+
if (!declared) return undefined;
|
|
66
|
+
let installed;
|
|
67
|
+
try { installed = JSON.parse(readFileSync(join(root, 'node_modules', packageName, 'package.json'), 'utf8')).version; } catch {}
|
|
68
|
+
return { declared, installed, ready: installed !== undefined && (installed === declared || !exactVersion.test(declared)) };
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
71
|
+
// What runs a checkout's verbs: `entry` is the dispatcher (the verbs
|
|
72
|
+
// themselves) and, for a pinned package, `launcher` is its own rer command.
|
|
73
|
+
// 1. the checkout's own pinned package, once node_modules holds the version
|
|
74
|
+
// package.json pins (see pinnedPackage), 2. its vendored scripts/rer.mjs, 3. this package's
|
|
75
|
+
// bundled dispatcher.
|
|
65
76
|
export function dispatcher(root) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
const pinned = root ? pinnedPackage(root) : undefined;
|
|
78
|
+
if (pinned?.ready) {
|
|
79
|
+
const location = realpathSync(join(root, 'node_modules', packageName));
|
|
80
|
+
// This package is that pin: it runs the checkout itself, whatever else the checkout holds.
|
|
81
|
+
if (location === ownRoot) return { mode: 'bundled', entry: join(ownRoot, 'bin/rer.mjs') };
|
|
82
|
+
if (existsSync(join(location, 'global/rer.mjs'))) return { mode: 'package', entry: join(location, 'bin/rer.mjs'), launcher: join(location, 'global/rer.mjs') };
|
|
69
83
|
}
|
|
70
84
|
if (root && existsSync(join(root, 'scripts/rer.mjs'))) return { mode: 'vendored', entry: join(root, 'scripts/rer.mjs') };
|
|
71
85
|
return { mode: 'bundled', entry: join(ownRoot, 'bin/rer.mjs') };
|