@jimhoyd/urlcode 0.4.0-alpha.2 → 0.4.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/.claude/skills/urlcode-authoring/SKILL.md +17 -19
- package/.claude/skills/urlcode-operations/SKILL.md +9 -9
- package/.claude-plugin/marketplace.json +1 -1
- package/CONTRIBUTING.md +20 -16
- package/README.md +59 -64
- package/ROADMAP.md +66 -376
- package/dist/BUILD-MANIFEST.json +26 -25
- package/dist/agents-guide.js +6 -6
- package/dist/authoring.js +15 -1
- package/dist/build-static.js +2 -0
- package/dist/capability-query.js +0 -1
- package/dist/catalog.js +0 -1
- package/dist/cli.js +25 -9
- package/dist/config.js +1 -1
- package/dist/explain-cli.js +4 -2
- package/dist/explain.js +8 -2
- package/dist/extensions.js +1 -1
- package/dist/http-response.js +1 -1
- package/dist/index.js +1 -0
- package/dist/init-with.js +36 -11
- package/dist/manifest.js +8 -2
- package/dist/mcp-authoring.js +4 -4
- package/dist/mcp.js +1 -1
- package/dist/policies/cache.js +2 -2
- package/dist/policies.js +3 -1
- package/dist/prerender.js +4 -0
- package/dist/project-dependencies.js +305 -0
- package/dist/readiness.js +3 -0
- package/dist/route-diff.js +12 -5
- package/dist/runtime.js +1 -1
- package/dist/trusted-functions.js +4 -5
- package/dist/types/authoring.d.ts +9 -1
- package/dist/types/capability-query.d.ts +0 -1
- package/dist/types/catalog.d.ts +0 -4
- package/dist/types/config.d.ts +1 -9
- package/dist/types/explain.d.ts +6 -1
- package/dist/types/extensions.d.ts +1 -1
- package/dist/types/http-response.d.ts +0 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/init-with.d.ts +7 -13
- package/dist/types/manifest.d.ts +5 -2
- package/dist/types/project-dependencies.d.ts +78 -0
- package/dist/types/readiness.d.ts +2 -0
- package/dist/types/trusted-functions.d.ts +1 -4
- package/dist/types/types.d.ts +8 -1
- package/dist/types.js +8 -1
- package/dist/typescript-authoring.js +18 -7
- package/docs/AI-AUTHORING.md +15 -7
- package/docs/ASSETS.md +2 -1
- package/docs/AUTH-BACKUP.md +32 -0
- package/docs/AWS.md +9 -0
- package/docs/BEST-PRACTICES.md +17 -9
- package/docs/CAPABILITIES.md +1 -1
- package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
- package/docs/CI.md +8 -3
- package/docs/CODEBASE-AUDIT-2026-09-20.md +284 -0
- package/docs/COMPOSING-A-SITE.md +278 -0
- package/docs/DEVELOPMENT-PIPELINE.md +270 -0
- package/docs/EXTENSIONS.md +51 -11
- package/docs/FRAMEWORK.md +67 -48
- package/docs/FUNCTION-SECURITY.md +44 -0
- package/docs/INSTALL.md +13 -8
- package/docs/LOCAL-DEVELOPMENT.md +3 -0
- package/docs/MIDDLEWARE.md +10 -4
- package/docs/OPEN-DECISIONS.md +224 -212
- package/docs/OPERATIONAL-PROOF.md +3 -3
- package/docs/OPERATIONS.md +3 -3
- package/docs/POLICIES.md +13 -5
- package/docs/PRERENDER.md +23 -11
- package/docs/PROJECT-DIRECTION.md +3 -3
- package/docs/READINESS.md +10 -5
- package/docs/README.md +20 -44
- package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
- package/docs/RELEASE-0.4.1.md +73 -0
- package/docs/RELEASE-READINESS.md +6 -6
- package/docs/RELEASE-SECURITY.md +96 -181
- package/docs/RESILIENCE.md +3 -3
- package/docs/ROUTING.md +3 -4
- package/docs/SECURITY-AUDIT.md +2 -2
- package/docs/SPECIFICATION.md +12 -6
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
- package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
- package/docs/SPIKE-CORE-LAYERING.md +49 -18
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
- package/docs/STARTERS.md +17 -5
- package/docs/STATIC.md +14 -3
- package/docs/TOOLING.md +10 -7
- package/docs/TYPESCRIPT-AUTHORING.md +6 -1
- package/docs/VERCEL.md +10 -2
- package/docs/VERSION-ALIGNMENT.md +76 -201
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
- package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
- package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
- package/docs/archive/2026-09-19/ROADMAP.md +387 -0
- package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +11 -0
- package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +365 -0
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +778 -0
- package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
- package/docs/archive/README.md +28 -0
- package/docs/policies/agents.md +1 -1
- package/docs/policies/compression.md +3 -2
- package/docs/policies/security.md +3 -2
- package/docs/yaml/functions.md +10 -2
- package/docs/yaml/middleware.md +5 -3
- package/examples/assets/example.yaml +1 -1
- package/examples/cookbook/middleware/envelope.mjs +4 -2
- package/examples/cookbook/route-index.json +1 -1
- package/examples/cookbook/routes/middleware.yaml +1 -1
- package/examples/prerender/README.md +14 -6
- package/examples/prerender/functions/page.mjs +4 -2
- package/examples/prerender/middleware/template.mjs +1 -1
- package/examples/prerender/prerender.mjs +1 -1
- package/examples/prerender/urlcode.yaml +8 -4
- package/llms-full.txt +503 -88
- package/llms.txt +6 -4
- package/package.json +27 -4
- package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
- package/recipes/authenticated-json-api/README.md +4 -3
- package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
- package/recipes/authenticated-json-api/recipe.yaml +1 -1
- package/recipes/contact-form/functions/contact.mjs +2 -1
- package/recipes/contact-form/recipe.yaml +2 -2
- package/recipes/cors-api/README.md +2 -2
- package/recipes/cors-api/recipe.yaml +1 -1
- package/recipes/health-page/README.md +1 -1
- package/recipes/json-api/README.md +1 -1
- package/recipes/json-api/recipe.yaml +3 -3
- package/recipes/middleware/README.md +8 -4
- package/recipes/middleware/middleware/envelope.mjs +4 -2
- package/recipes/protected-download/README.md +1 -1
- package/recipes/protected-download/recipe.yaml +1 -1
- package/recipes/static-plus-api/README.md +2 -2
- package/recipes/static-plus-api/public/index.html +1 -1
- package/recipes/static-plus-api/recipe.yaml +1 -1
- package/recipes/static-plus-api/urlcode.yaml +1 -1
- package/recipes/typescript/recipe.yaml +4 -4
- package/skills/urlcode/SKILL.md +6 -6
- package/starters/default/AGENTS.md +6 -6
- package/docs/SPIKE-LAMBDA-COMPILE.md +0 -201
- package/docs/SPIKE-MONOREPO.md +0 -322
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { ConfigError, assert } from './errors.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Exact dependency pins for a generated application.
|
|
8
|
+
*
|
|
9
|
+
* `urlcode init --with` resolves whatever `@jimhoyd/urlcode-<name>` packages are already installed beside the
|
|
10
|
+
* invoking directory. Without a manifest the generated site records nothing about which versions it was built
|
|
11
|
+
* against, so a later `npm install @jimhoyd/urlcode-auth` in that site can resolve a different set (#212). This
|
|
12
|
+
* module reads the versions that were actually resolved, validates the whole set against the packages' own
|
|
13
|
+
* declared `peerDependencies`, and renders a `package.json` pinning every one of them exactly.
|
|
14
|
+
*
|
|
15
|
+
* It never runs a package manager: generating a lockfile stays an explicit `npm install` the operator runs after
|
|
16
|
+
* reviewing the manifest. It also never imports an extension implementation -- only package metadata is read, by
|
|
17
|
+
* a name the caller supplied -- so core's generic extension boundary is unchanged.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const CORE_PACKAGE = '@jimhoyd/urlcode';
|
|
21
|
+
const namePattern = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const versionPattern = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
25
|
+
|
|
26
|
+
export function parseVersion(value ) {
|
|
27
|
+
const match = versionPattern.exec(value.trim());
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
const pre = match[4] === undefined ? [] : match[4].split('.').map(part => /^\d+$/.test(part) ? Number(part) : part);
|
|
30
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), pre };
|
|
31
|
+
}
|
|
32
|
+
function comparePre(a , b ) {
|
|
33
|
+
// A version with a prerelease is lower than the same version without one.
|
|
34
|
+
if (!a.length || !b.length) return a.length === b.length ? 0 : a.length ? -1 : 1;
|
|
35
|
+
for (let index = 0; index < Math.max(a.length, b.length); index++) {
|
|
36
|
+
const left = a[index], right = b[index];
|
|
37
|
+
if (left === undefined) return -1;
|
|
38
|
+
if (right === undefined) return 1;
|
|
39
|
+
if (left === right) continue;
|
|
40
|
+
if (typeof left === 'number' && typeof right === 'number') return left < right ? -1 : 1;
|
|
41
|
+
if (typeof left === 'number') return -1; // numeric identifiers rank lower than alphanumeric ones
|
|
42
|
+
if (typeof right === 'number') return 1;
|
|
43
|
+
return left < right ? -1 : 1;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
export function compareVersions(a , b ) {
|
|
48
|
+
for (const key of ['major', 'minor', 'patch'] ) if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
49
|
+
return comparePre(a.pre, b.pre);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A deliberately small subset of the range grammar: `*`, exact versions, the comparators, `^` and `~` over a
|
|
54
|
+
* complete `x.y.z`, whitespace for AND and `||` for OR. Anything else refuses rather than guessing, so an
|
|
55
|
+
* unrecognized peer range surfaces as a refusal instead of a silently wrong compatibility answer.
|
|
56
|
+
*/
|
|
57
|
+
function parseComparatorSet(text , context ) {
|
|
58
|
+
const tokens = text.trim().split(/\s+/).filter(Boolean);
|
|
59
|
+
if (!tokens.length || tokens.every(token => token === '*' || token === 'x' || token === 'X')) return 'any';
|
|
60
|
+
const comparators = [];
|
|
61
|
+
for (const token of tokens) {
|
|
62
|
+
const match = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(token);
|
|
63
|
+
const version = match ? parseVersion(match[2] ) : null;
|
|
64
|
+
if (!match || !version) throw new ConfigError(`Unsupported version range ${JSON.stringify(text)} (${context}); supported forms are *, x.y.z, >=, >, <, <=, = and ^ or ~ over a complete x.y.z`);
|
|
65
|
+
const operator = (match[1] ?? '=') ;
|
|
66
|
+
if (operator === '^' || operator === '~') {
|
|
67
|
+
// ^0.x is minor-bounded, ^0.0.x is patch-bounded, ^x is major-bounded; ~ is always minor-bounded.
|
|
68
|
+
const upper = operator === '~' || version.major === 0
|
|
69
|
+
? (operator === '^' && version.major === 0 && version.minor === 0
|
|
70
|
+
? { major: 0, minor: 0, patch: version.patch + 1, pre: [] }
|
|
71
|
+
: { major: version.major, minor: version.minor + 1, patch: 0, pre: [] })
|
|
72
|
+
: { major: version.major + 1, minor: 0, patch: 0, pre: [] };
|
|
73
|
+
comparators.push({ operator: '>=', version }, { operator: '<', version: upper });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
comparators.push({ operator, version });
|
|
77
|
+
}
|
|
78
|
+
return comparators;
|
|
79
|
+
}
|
|
80
|
+
function satisfiesComparators(version , comparators ) {
|
|
81
|
+
// npm's prerelease rule: a prerelease version only satisfies a set that itself names a prerelease of the same
|
|
82
|
+
// x.y.z, so 0.5.0-alpha.1 never slips past `<0.5.0`.
|
|
83
|
+
if (version.pre.length && !comparators.some(item => item.version.pre.length && item.version.major === version.major && item.version.minor === version.minor && item.version.patch === version.patch)) return false;
|
|
84
|
+
return comparators.every(item => {
|
|
85
|
+
const order = compareVersions(version, item.version);
|
|
86
|
+
switch (item.operator) {
|
|
87
|
+
case '>': return order > 0;
|
|
88
|
+
case '>=': return order >= 0;
|
|
89
|
+
case '<': return order < 0;
|
|
90
|
+
case '<=': return order <= 0;
|
|
91
|
+
default: return order === 0;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function satisfiesRange(version , range , context = 'peer range') {
|
|
96
|
+
const parsed = parseVersion(version);
|
|
97
|
+
if (!parsed) throw new ConfigError(`Unsupported version ${JSON.stringify(version)} (${context})`);
|
|
98
|
+
return range.split('||').some(part => {
|
|
99
|
+
const set = parseComparatorSet(part, context);
|
|
100
|
+
return set === 'any' || satisfiesComparators(parsed, set);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
const record = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
110
|
+
const strings = (value ) => {
|
|
111
|
+
const out = {};
|
|
112
|
+
if (record(value)) for (const [key, item] of Object.entries(value)) if (typeof item === 'string') out[key] = item;
|
|
113
|
+
return out;
|
|
114
|
+
};
|
|
115
|
+
async function readManifest(file ) {
|
|
116
|
+
let parsed ;
|
|
117
|
+
try { parsed = JSON.parse(await readFile(file, 'utf8')) ; }
|
|
118
|
+
catch { return null; }
|
|
119
|
+
if (typeof parsed.name !== 'string' || typeof parsed.version !== 'string') return null;
|
|
120
|
+
const optional = new Set ();
|
|
121
|
+
if (record(parsed.peerDependenciesMeta)) for (const [key, value] of Object.entries(parsed.peerDependenciesMeta)) if (record(value) && value.optional === true) optional.add(key);
|
|
122
|
+
const engines = record(parsed.engines) && typeof parsed.engines.node === 'string' ? parsed.engines.node : undefined;
|
|
123
|
+
return { name: parsed.name, version: parsed.version, directory: dirname(file), peers: strings(parsed.peerDependencies), optionalPeers: optional, node: engines };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Walks `node_modules` upwards from the invoking directory, exactly like Node's own resolution but reading the
|
|
127
|
+
* package's manifest rather than its entry point. Reading the manifest directly (instead of resolving the entry)
|
|
128
|
+
* means a package whose `exports` does not expose `./package.json` is still inspectable, and nothing in the
|
|
129
|
+
* package is loaded or executed.
|
|
130
|
+
*/
|
|
131
|
+
export async function findInstalledPackage(name , from ) {
|
|
132
|
+
assert(namePattern.test(name), `Invalid package name: ${name}`);
|
|
133
|
+
let directory = resolve(from);
|
|
134
|
+
for (;;) {
|
|
135
|
+
const found = await readManifest(join(directory, 'node_modules', ...name.split('/'), 'package.json'));
|
|
136
|
+
if (found && found.name === name) return found;
|
|
137
|
+
const parent = dirname(directory);
|
|
138
|
+
if (parent === directory) return null;
|
|
139
|
+
directory = parent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** The version of the runtime executing this command; that is the version a generated site is pinned to. */
|
|
143
|
+
export async function runningCore() {
|
|
144
|
+
const file = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
145
|
+
const manifest = await readManifest(file);
|
|
146
|
+
assert(manifest && manifest.name === CORE_PACKAGE, `Could not read the running runtime manifest at ${file}`);
|
|
147
|
+
return manifest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* `resolved`/`link` entries from npm's hidden lockfile, used only to notice that a package was installed from a
|
|
152
|
+
* local directory or tarball. Such a package's version number is not installable from a registry, so the manifest
|
|
153
|
+
* has to record the local specifier instead of the exact version for the site to install offline.
|
|
154
|
+
*/
|
|
155
|
+
async function localSpecifiers(cwd ) {
|
|
156
|
+
const out = new Map ();
|
|
157
|
+
let parsed ;
|
|
158
|
+
try { parsed = JSON.parse(await readFile(join(cwd, 'node_modules', '.package-lock.json'), 'utf8')); }
|
|
159
|
+
catch { return out; }
|
|
160
|
+
if (!record(parsed) || !record(parsed.packages)) return out;
|
|
161
|
+
for (const [key, value] of Object.entries(parsed.packages)) {
|
|
162
|
+
const index = key.lastIndexOf('node_modules/');
|
|
163
|
+
if (index !== 0 || !record(value)) continue; // nested installs belong to another package's tree
|
|
164
|
+
const name = key.slice('node_modules/'.length);
|
|
165
|
+
const resolvedTo = typeof value.resolved === 'string' ? value.resolved : '';
|
|
166
|
+
if (value.link === true) { if (resolvedTo) out.set(name, 'file:' + resolve(cwd, resolvedTo)); continue; }
|
|
167
|
+
if (resolvedTo.startsWith('file:')) out.set(name, 'file:' + resolve(cwd, resolvedTo.slice('file:'.length)));
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
export function parsePin(value ) {
|
|
194
|
+
const index = value.indexOf('=');
|
|
195
|
+
assert(index > 0, 'Use --pin <package>=<specifier>, for example --pin @jimhoyd/urlcode-auth=file:/abs/path/urlcode-auth.tgz');
|
|
196
|
+
const name = value.slice(0, index).trim(), specifier = value.slice(index + 1).trim();
|
|
197
|
+
assert(namePattern.test(name), `Invalid --pin package name: ${name}`);
|
|
198
|
+
assert(specifier.length > 0 && specifier.length <= 512 && !/[\s\0]/.test(specifier), `Invalid --pin specifier for ${name}`);
|
|
199
|
+
return [name, specifier];
|
|
200
|
+
}
|
|
201
|
+
function nodeFloor(ranges ) {
|
|
202
|
+
let best ;
|
|
203
|
+
for (const range of ranges) {
|
|
204
|
+
// Only a plain `>=x.y.z` floor is recognized; anything else is left out rather than reinterpreted.
|
|
205
|
+
const match = range === undefined ? null : /^>=\s*(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(range.trim());
|
|
206
|
+
const version = match ? parseVersion(match[1] ) : null;
|
|
207
|
+
if (version && (!best || compareVersions(version, best) > 0)) best = version;
|
|
208
|
+
}
|
|
209
|
+
return best ? `>=${best.major}.${best.minor}.${best.patch}${best.pre.length ? '-' + best.pre.join('.') : ''}` : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolves core plus every named extension and their declared peers, then validates the whole set against every
|
|
214
|
+
* declared peer range before returning. Compatibility is judged as a set: a mismatch anywhere refuses, listing
|
|
215
|
+
* every mismatch rather than the first.
|
|
216
|
+
*/
|
|
217
|
+
export async function collectDependencySet(names , packageNames , { cwd = process.cwd(), overrides } = {}) {
|
|
218
|
+
assert(names.length === packageNames.length, 'Each extension name needs its package name');
|
|
219
|
+
const core = await runningCore();
|
|
220
|
+
const resolved = new Map ([[CORE_PACKAGE, core]]);
|
|
221
|
+
const roles = new Map ([[CORE_PACKAGE, 'runtime']]);
|
|
222
|
+
const missing = [];
|
|
223
|
+
const pending = [];
|
|
224
|
+
for (const pkg of packageNames) { roles.set(pkg, 'extension'); pending.push(pkg); }
|
|
225
|
+
// A copy of core installed beside the project would be resolved by the generated site, not the one running
|
|
226
|
+
// here; pinning one version while the other is installed is exactly the inconsistency this refuses to record.
|
|
227
|
+
const installedCore = await findInstalledPackage(CORE_PACKAGE, cwd);
|
|
228
|
+
if (installedCore && installedCore.version !== core.version)
|
|
229
|
+
throw new ConfigError(`${CORE_PACKAGE} ${installedCore.version} is installed in ${cwd} but this command is ${core.version}; run the matching CLI or align the installed runtime before recording pins`);
|
|
230
|
+
while (pending.length) {
|
|
231
|
+
const name = pending.shift() ;
|
|
232
|
+
if (resolved.has(name)) continue;
|
|
233
|
+
const found = await findInstalledPackage(name, cwd);
|
|
234
|
+
if (!found) { missing.push(name); continue; }
|
|
235
|
+
resolved.set(name, found);
|
|
236
|
+
for (const peer of Object.keys(found.peers)) {
|
|
237
|
+
if (resolved.has(peer) || found.optionalPeers.has(peer)) continue;
|
|
238
|
+
if (!roles.has(peer)) roles.set(peer, 'peer');
|
|
239
|
+
pending.push(peer);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (missing.length) {
|
|
243
|
+
const required = missing.map(name => {
|
|
244
|
+
const source = [...resolved.values()].find(pkg => Object.hasOwn(pkg.peers, name));
|
|
245
|
+
return source ? `${name} (required by ${source.name} ${source.peers[name]})` : name;
|
|
246
|
+
});
|
|
247
|
+
throw new ConfigError(`Cannot record exact pins: ${required.join(', ')} ${missing.length > 1 ? 'are' : 'is'} not installed in ${cwd}. Install the missing package(s) there, or pass --no-manifest to generate the site without a dependency manifest.`);
|
|
248
|
+
}
|
|
249
|
+
const conflicts = [];
|
|
250
|
+
for (const pkg of resolved.values())
|
|
251
|
+
for (const [peer, range] of Object.entries(pkg.peers)) {
|
|
252
|
+
const installed = resolved.get(peer);
|
|
253
|
+
if (!installed) continue; // optional peer that is not installed
|
|
254
|
+
if (!satisfiesRange(installed.version, range, `${pkg.name} peerDependencies.${peer}`)) conflicts.push(`${pkg.name} ${pkg.version} requires ${peer} ${range}, but ${installed.version} is installed`);
|
|
255
|
+
}
|
|
256
|
+
if (conflicts.length) throw new ConfigError(`Incompatible versions: ${conflicts.join('; ')}`);
|
|
257
|
+
const locals = await localSpecifiers(cwd);
|
|
258
|
+
const pins = [...resolved.values()]
|
|
259
|
+
.map(pkg => {
|
|
260
|
+
const override = overrides?.get(pkg.name);
|
|
261
|
+
const local = locals.get(pkg.name);
|
|
262
|
+
const specifier = override ?? local ?? pkg.version;
|
|
263
|
+
return { name: pkg.name, version: pkg.version, specifier, local: specifier !== pkg.version, role: roles.get(pkg.name) ?? 'peer' };
|
|
264
|
+
})
|
|
265
|
+
.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
266
|
+
for (const [name] of overrides ?? []) assert(pins.some(pin => pin.name === name), `--pin ${name} names a package that is not part of this project's dependency set`);
|
|
267
|
+
const dependencies = {};
|
|
268
|
+
for (const pin of pins) dependencies[pin.name] = pin.specifier;
|
|
269
|
+
return { pins, dependencies, node: nodeFloor([...resolved.values()].map(pkg => pkg.node)), local: pins.some(pin => pin.local) };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const TRIMMED = '._-';
|
|
273
|
+
const manifestName = (directory ) => {
|
|
274
|
+
const mapped = (directory.split(/[\\/]/).pop() ?? 'urlcode-site').toLowerCase().replace(/[^a-z0-9._-]+/g, '-');
|
|
275
|
+
// Trimmed with indices rather than /^[._-]+|[-._]+$/: an anchored quantifier
|
|
276
|
+
// over a repeated character is retried from every start position, which is
|
|
277
|
+
// quadratic on a directory name of many dashes (CodeQL js/polynomial-redos).
|
|
278
|
+
let start = 0, end = mapped.length;
|
|
279
|
+
while (start < end && TRIMMED.includes(mapped[start] ?? '')) start += 1;
|
|
280
|
+
while (end > start && TRIMMED.includes(mapped[end - 1] ?? '')) end -= 1;
|
|
281
|
+
const base = mapped.slice(start, end);
|
|
282
|
+
return base.length ? base.slice(0, 214) : 'urlcode-site';
|
|
283
|
+
};
|
|
284
|
+
/** The generated manifest: private, module type, exact pins, and nothing that runs a package manager. */
|
|
285
|
+
export function renderPackageManifest(directory , set ) {
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
name: manifestName(directory),
|
|
288
|
+
private: true,
|
|
289
|
+
version: '0.0.0',
|
|
290
|
+
type: 'module',
|
|
291
|
+
...(set.node ? { engines: { node: set.node } } : {}),
|
|
292
|
+
dependencies: set.dependencies,
|
|
293
|
+
}, null, 2) + '\n';
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The install step is printed, never run: generating `package-lock.json` executes a package manager, which
|
|
297
|
+
* resolves and downloads code, so it stays the operator's explicit action after reviewing the manifest.
|
|
298
|
+
*/
|
|
299
|
+
export function installSteps(directory , set ) {
|
|
300
|
+
const where = isAbsolute(directory) ? directory : resolve(directory);
|
|
301
|
+
return [
|
|
302
|
+
`Review ${join(where, 'package.json')}; it pins ${set.pins.map(pin => `${pin.name}@${pin.version}`).join(', ')}.`,
|
|
303
|
+
`Run \`npm install\` in ${where} to install those exact versions and generate package-lock.json${set.local ? ' (add `--offline` when the local paths are your only source)' : ''}. urlcode never runs a package manager for you.`,
|
|
304
|
+
];
|
|
305
|
+
}
|
package/dist/readiness.js
CHANGED
|
@@ -17,6 +17,8 @@ import { runCompliance } from './compliance.js';
|
|
|
17
17
|
/** One configured route as the inventory reports it: a PlanInventoryEntry with the handler kind named. */
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
|
|
@@ -74,6 +76,7 @@ export function projectPlan(compiled )
|
|
|
74
76
|
const now = Date.now();
|
|
75
77
|
const inventory = routes.map(route => { const advisories = routeAdvisories(route); return { path:route.pattern, handler:handlers.find(key => route[key]), methods:route.methods, middleware:route.middleware?.length || 0,
|
|
76
78
|
policies:[...(route.policy ? Object.keys(route.policy.describe) : []),...(route.extensionPolicyNames??[]).map(name=>`extensions.${name}`)],
|
|
79
|
+
sandbox:route.sandbox === true, ...(route.sandboxReason ? { sandboxReason:route.sandboxReason } : {}),
|
|
77
80
|
...(route.generated ? { generated:route.generated } : {}),
|
|
78
81
|
...(advisories.length ? { advisories } : {}),
|
|
79
82
|
state:route.enabled === false ? 'disabled' : route.expiresAt && now >= route.expiresAt ? 'expired' : 'active' }; });
|
package/dist/route-diff.js
CHANGED
|
@@ -17,7 +17,7 @@ const isStrings = (value ) => Array.isArray(value) &&
|
|
|
17
17
|
function entry(value , index ) {
|
|
18
18
|
const fail = (what ) => { throw new ConfigError(`Route report inventory[${index}] ${what}`); };
|
|
19
19
|
if (!isRecord(value)) return fail('is not an object');
|
|
20
|
-
const { path, handler, methods, middleware, policies, generated, state } = value;
|
|
20
|
+
const { path, handler, methods, middleware, policies, generated, state, sandbox, sandboxReason } = value;
|
|
21
21
|
if (typeof path !== 'string' || !path) return fail('needs a path');
|
|
22
22
|
if (handler !== undefined && typeof handler !== 'string') return fail('has an invalid handler');
|
|
23
23
|
if (!isStrings(methods)) return fail('needs methods');
|
|
@@ -25,7 +25,13 @@ function entry(value , index ) {
|
|
|
25
25
|
if (!isStrings(policies)) return fail('needs policies');
|
|
26
26
|
if (generated !== undefined && typeof generated !== 'string') return fail('has an invalid generated marker');
|
|
27
27
|
if (typeof state !== 'string' || !states.includes(state )) return fail('needs a state');
|
|
28
|
-
|
|
28
|
+
// `sandbox`/`sandboxReason` postdate the report format, so an older report
|
|
29
|
+
// omits them: absent is carried through as absent rather than defaulted to
|
|
30
|
+
// `false`, which would read as a trust change that never happened.
|
|
31
|
+
if (sandbox !== undefined && typeof sandbox !== 'boolean') return fail('has an invalid sandbox flag');
|
|
32
|
+
if (sandboxReason !== undefined && typeof sandboxReason !== 'string') return fail('has an invalid sandboxReason');
|
|
33
|
+
return { path, handler, methods, middleware, policies, ...(generated !== undefined ? { generated } : {}), state:state ,
|
|
34
|
+
...(sandbox !== undefined ? { sandbox } : {}), ...(sandboxReason !== undefined ? { sandboxReason } : {}) };
|
|
29
35
|
}
|
|
30
36
|
/** Validates a parsed `urlcode routes` JSON report (a child-process or file boundary) into a snapshot. */
|
|
31
37
|
export function parseRouteSnapshot(value ) {
|
|
@@ -71,7 +77,7 @@ export function diffRoutes(before , after )
|
|
|
71
77
|
}
|
|
72
78
|
export const hasRouteChanges = (diff ) => diff.added.length + diff.removed.length + diff.changed.length > 0;
|
|
73
79
|
|
|
74
|
-
const fields = ['handler','methods','state','middleware','policies','generated','policy'] ;
|
|
80
|
+
const fields = ['handler','methods','state','sandbox','sandboxReason','middleware','policies','generated','policy'] ;
|
|
75
81
|
|
|
76
82
|
const cell = (value ) => {
|
|
77
83
|
const text = value === undefined ? '' : Array.isArray(value) && value.every(item => typeof item === 'string') ? value.join(', ') : typeof value === 'string' ? value : canonical(value);
|
|
@@ -82,12 +88,13 @@ const code = (value ) => `\`${value.replace(/\\/g, '\\\\').replac
|
|
|
82
88
|
function table(headers , rows ) {
|
|
83
89
|
return [`| ${headers.join(' | ')} |`, `|${headers.map(() => '---').join('|')}|`, ...rows.map(row => `| ${row.join(' | ')} |`)];
|
|
84
90
|
}
|
|
85
|
-
const routeRow = (record ) => [code(record.path), cell(record.handler), cell(record.methods), cell(record.state),
|
|
91
|
+
const routeRow = (record ) => [code(record.path), cell(record.handler), cell(record.methods), cell(record.state),
|
|
92
|
+
cell(record.sandbox), cell(record.sandboxReason), String(record.middleware), cell(record.policies), cell(record.generated)];
|
|
86
93
|
/** Renders a diff as Markdown: one table per nonempty section, or "No route changes". */
|
|
87
94
|
export function renderRouteDiff(diff ) {
|
|
88
95
|
if (!hasRouteChanges(diff)) return 'No route changes\n';
|
|
89
96
|
const lines = [];
|
|
90
|
-
const routeHeaders = ['Route','Handler','Methods','State','Middleware','Policies','Generated'];
|
|
97
|
+
const routeHeaders = ['Route','Handler','Methods','State','Sandbox','Sandbox reason','Middleware','Policies','Generated'];
|
|
91
98
|
const section = (title , list ) => {
|
|
92
99
|
if (!list.length) return;
|
|
93
100
|
lines.push(`### ${title} (${list.length})`, '', ...table(routeHeaders, list.map(routeRow)), '');
|
package/dist/runtime.js
CHANGED
|
@@ -117,7 +117,7 @@ export async function createRuntime(project , rawOptions
|
|
|
117
117
|
// route is trusted-by-default and dispatches through `trusted` below,
|
|
118
118
|
// in-process, with no worker or WASM engine involved at all.
|
|
119
119
|
const pool=await new FunctionPool(routes.filter(route=>route.sandbox===true), { root:loaded.root, snapshot, log:options.log, workers:options.workers, timeoutMs:options.timeoutMs, maxBytes:options.maxBytes }).start();
|
|
120
|
-
const trusted = new TrustedFunctions({ timeoutMs: options.timeoutMs, maxBytes: options.maxBytes
|
|
120
|
+
const trusted = new TrustedFunctions({ timeoutMs: options.timeoutMs, maxBytes: options.maxBytes });
|
|
121
121
|
// Eagerly validated up front, exactly like the sandboxed pool above: a
|
|
122
122
|
// trusted route with a broken module or a missing export fails activation
|
|
123
123
|
// here rather than on its first request.
|
|
@@ -33,16 +33,15 @@ import { routeFunctions } from './function-sources.js';
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
|
|
39
38
|
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
export class TrustedFunctions {
|
|
45
|
-
timeoutMs ; maxBytes ;
|
|
44
|
+
timeoutMs ; maxBytes ;
|
|
46
45
|
// Node's ESM loader caches a resolved module forever by URL, unlike a
|
|
47
46
|
// sandboxed worker, which gets a genuinely fresh module registry on every
|
|
48
47
|
// reload/restart. A snapshot reload constructs a brand-new TrustedFunctions
|
|
@@ -51,8 +50,8 @@ export class TrustedFunctions {
|
|
|
51
50
|
// sandboxed pool's "new workers, new snapshot" reload contract, while a
|
|
52
51
|
// single instance still only imports each module once per process.
|
|
53
52
|
epoch = randomUUID();
|
|
54
|
-
constructor({ timeoutMs = 5000, maxBytes = 1048576
|
|
55
|
-
this.timeoutMs = timeoutMs; this.maxBytes = maxBytes;
|
|
53
|
+
constructor({ timeoutMs = 5000, maxBytes = 1048576 } = {}) {
|
|
54
|
+
this.timeoutMs = timeoutMs; this.maxBytes = maxBytes;
|
|
56
55
|
}
|
|
57
56
|
// Eagerly imports and validates every declared export exists as a function,
|
|
58
57
|
// the same guarantee FunctionPool.start() gives the sandboxed path: a
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import type { DependencySet } from './project-dependencies.ts';
|
|
2
|
+
export interface InitOptions {
|
|
3
|
+
/**
|
|
4
|
+
* When given, a `package.json` pinning exactly these versions is written beside `urlcode.yaml`. Route-only
|
|
5
|
+
* initialization stays the default: a project whose runtime is managed elsewhere gets no manifest at all.
|
|
6
|
+
*/
|
|
7
|
+
manifest?: DependencySet | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function initProject(destination: string, { manifest }?: InitOptions): Promise<string>;
|
|
2
10
|
export declare function addRedirect(project: string, destination: string, alias?: string | undefined): Promise<string>;
|
|
@@ -17,7 +17,6 @@ export interface CapabilityEntry extends CapabilityDetail {
|
|
|
17
17
|
recipes: CapabilityUsage[];
|
|
18
18
|
cookbook: CapabilityUsage[];
|
|
19
19
|
}
|
|
20
|
-
export declare function capabilityNameList(): readonly CapabilityName[];
|
|
21
20
|
/** One catalog entry with its schema fragments and bundled usage. No project, credentials or network are read. */
|
|
22
21
|
export declare function getCapability(name: string): CapabilityEntry;
|
|
23
22
|
export declare function formatCapability(entry: CapabilityEntry): string;
|
package/dist/types/catalog.d.ts
CHANGED
|
@@ -46,10 +46,6 @@ export interface SearchHit<T extends CatalogMetadata> {
|
|
|
46
46
|
score: number;
|
|
47
47
|
matched: string[];
|
|
48
48
|
}
|
|
49
|
-
export declare const metadataFiles: {
|
|
50
|
-
readonly recipe: "recipe.yaml";
|
|
51
|
-
readonly example: "example.yaml";
|
|
52
|
-
};
|
|
53
49
|
/** Reads and schema-validates one metadata file; the id must equal the directory name, and file lists stay authoring-safe paths. */
|
|
54
50
|
export declare function readMetadata(root: string, id: string, file: 'recipe.yaml' | 'example.yaml'): Promise<CatalogMetadata>;
|
|
55
51
|
/** Preflight only: loads the project, expands site routes and asks the capability analysis for every target. No bindings, code or activation. */
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LoadedDocument, ProjectDocument, RouteConfig } from './types.ts';
|
|
2
2
|
/** What config-worker.ts posts back: the loaded document, or the ConfigError message. */
|
|
3
3
|
export type ConfigWorkerResult = {
|
|
4
4
|
value: LoadedDocument;
|
|
@@ -17,14 +17,6 @@ export declare const SHORT_FORM_PATH_SCHEMA: {
|
|
|
17
17
|
readonly minLength: 1;
|
|
18
18
|
readonly maxLength: 128;
|
|
19
19
|
};
|
|
20
|
-
/**
|
|
21
|
-
* Expands the YAML short forms into the canonical long form. `function: functions/x.mjs`
|
|
22
|
-
* becomes `{source, args}` with an argument per `{param}` in the path, declaring any
|
|
23
|
-
* parameter the route does not declare itself; a string middleware entry becomes `{source}`;
|
|
24
|
-
* a route-level `cache` becomes `policies.cache` (refused alongside a direct `policies.cache`).
|
|
25
|
-
* Everything downstream (routes, audit, the compiled table) sees only the long form.
|
|
26
|
-
*/
|
|
27
|
-
export declare function normalizeRoute(pattern: string, route: AuthoredRouteConfig | RouteConfig): RouteConfig;
|
|
28
20
|
export declare function safeFile(root: string, file: unknown): Promise<string>;
|
|
29
21
|
export declare function loadDocument(project: string, { timeoutMs }?: {
|
|
30
22
|
timeoutMs?: number;
|
package/dist/types/explain.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface RouteExplanation {
|
|
|
55
55
|
source: string;
|
|
56
56
|
export: string;
|
|
57
57
|
}[];
|
|
58
|
+
/** Execution mode for this route's whole `function`/`middleware` chain: `true` for the
|
|
59
|
+
* QuickJS sandbox, `false` for trusted in-process execution. Route-level, not handler-level:
|
|
60
|
+
* a native handler with `middleware` runs project code too, and its mode has to be reviewable.
|
|
61
|
+
* Reported for every route, including those that run no project code at all. */
|
|
62
|
+
sandbox: boolean;
|
|
63
|
+
sandboxReason?: string;
|
|
58
64
|
inputs: {
|
|
59
65
|
parameters: ExplainedParameter[];
|
|
60
66
|
body?: RequestBodyPolicy;
|
|
@@ -89,7 +95,6 @@ export interface ExplainOptions {
|
|
|
89
95
|
projectSha256?: string | undefined;
|
|
90
96
|
now?: number | undefined;
|
|
91
97
|
}
|
|
92
|
-
export declare function routeState(route: CompiledRoute, now: number): RouteState;
|
|
93
98
|
/** Describe one compiled route. `chain` is the policy chain compiled for it, when the project declares policies. */
|
|
94
99
|
export declare function explainCompiledRoute(loaded: LoadedDocument, route: CompiledRoute, chain: PolicyChain | undefined, options?: ExplainOptions): RouteExplanation;
|
|
95
100
|
export declare function nearestRoutes(target: string, patterns: Iterable<string>, limit?: number): string[];
|
|
@@ -83,7 +83,7 @@ export interface RuntimeExtension {
|
|
|
83
83
|
* extension response's header/size caps, exactly like `authorize`-gating
|
|
84
84
|
* auth/admin extensions. `false` is an explicit, reviewed opt-in a generic,
|
|
85
85
|
* cache-transparent extension (pure request/response middleware with no
|
|
86
|
-
* gating semantics of its own
|
|
86
|
+
* gating semantics of its own) makes to say its
|
|
87
87
|
* `middleware()` hook never depends on withholding the response from
|
|
88
88
|
* shared caches: the wrapped route's own declared cache headers pass
|
|
89
89
|
* through unchanged, exactly as the native `middleware:` array already
|
|
@@ -32,7 +32,6 @@ export interface ResponseWriter {
|
|
|
32
32
|
end(body?: ResponseBody): unknown;
|
|
33
33
|
destroy(): unknown;
|
|
34
34
|
}
|
|
35
|
-
export declare const forbiddenHeaders: Set<string>;
|
|
36
35
|
export declare function prepareResponse(result: HandlerResult, { requestId, method }: ResponseOptions): PreparedResponse;
|
|
37
36
|
export declare function writeResponse(res: ResponseWriter, result: HandlerResult, options: ResponseOptions): number;
|
|
38
37
|
export declare function errorResponse(error: unknown, { requestId, method, headers }: ResponseOptions & {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -38,4 +38,5 @@ export { scaffoldProject } from './scaffold.ts';
|
|
|
38
38
|
export type { ScaffoldReport, Unresolved as ScaffoldUnresolved } from './scaffold.ts';
|
|
39
39
|
export { initProject, addRedirect } from './authoring.ts';
|
|
40
40
|
export { initProjectWith } from './init-with.ts';
|
|
41
|
+
export { collectDependencySet, renderPackageManifest, installSteps } from './project-dependencies.ts';
|
|
41
42
|
export type { ScaffoldRequest, ScaffoldResult, ScaffoldFile } from './extensions.ts';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
/** Directory names inside the generated site. The route project lives under `app/`; everything else is operator-owned. */
|
|
3
|
-
export declare const PROJECT_DIRECTORY = "app", HOST_FILE = "host.mjs", ROUTES_FILE = "routes/extensions.yaml";
|
|
1
|
+
import type { DependencyPin } from './project-dependencies.ts';
|
|
4
2
|
export interface InitWithOptions {
|
|
5
3
|
cwd?: string | undefined;
|
|
4
|
+
/** Default true: record exact pins for core, the named extensions and their declared peers. */
|
|
5
|
+
manifest?: boolean | undefined;
|
|
6
|
+
/** `--pin <package>=<specifier>` overrides, for local tarballs, checkouts and mirrors. */
|
|
7
|
+
pins?: ReadonlyMap<string, string> | undefined;
|
|
6
8
|
}
|
|
7
9
|
export interface InitWithResult {
|
|
8
10
|
directory: string;
|
|
@@ -11,20 +13,12 @@ export interface InitWithResult {
|
|
|
11
13
|
extensions: string[];
|
|
12
14
|
projectSha256: string;
|
|
13
15
|
nextSteps: string[];
|
|
16
|
+
dependencies: DependencyPin[];
|
|
14
17
|
}
|
|
15
18
|
export declare function parseWithNames(value: string): string[];
|
|
16
|
-
export declare const packageName: (name: string) => string;
|
|
17
|
-
/**
|
|
18
|
-
* Resolves the extension package from the invoking directory (Node's package resolution with the default
|
|
19
|
-
* conditions), imports it, and calls its `scaffold` export. Nothing is bundled; core never imports these packages
|
|
20
|
-
* at build time. Refuses a missing package or a package without `scaffold` before anything is written.
|
|
21
|
-
*/
|
|
22
|
-
export declare function loadScaffold(name: string, request: ScaffoldRequest, cwd: string): Promise<ScaffoldResult>;
|
|
23
|
-
export declare function renderHost(names: readonly string[], results: readonly ScaffoldResult[]): string;
|
|
24
|
-
export declare function renderReadme(directory: string, names: readonly string[], results: readonly ScaffoldResult[], starter: string, env: Record<string, string>, projectSha256: string): string;
|
|
25
19
|
/**
|
|
26
20
|
* `urlcode init <directory> --with a,b`: the starter under `app/`, every extension's fragments merged into one
|
|
27
21
|
* `urlcode.yaml`, one `host.mjs`, one `README.md` and the extensions' own files. All packages are resolved and
|
|
28
22
|
* their scaffolds computed before anything is written, so a refusal leaves no directory behind.
|
|
29
23
|
*/
|
|
30
|
-
export declare function initProjectWith(destination: string, names: readonly string[], { cwd }?: InitWithOptions): Promise<InitWithResult>;
|
|
24
|
+
export declare function initProjectWith(destination: string, names: readonly string[], { cwd, manifest, pins }?: InitWithOptions): Promise<InitWithResult>;
|
package/dist/types/manifest.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CapabilityName, CapabilityTarget } from './capabilities.ts';
|
|
2
2
|
import type { InspectOptions } from './tooling.ts';
|
|
3
3
|
import type { RouteExplanation } from './explain.ts';
|
|
4
|
-
export declare const MANIFEST_SCHEMA_VERSION =
|
|
4
|
+
export declare const MANIFEST_SCHEMA_VERSION = 3;
|
|
5
5
|
export interface ManifestRoute {
|
|
6
6
|
path: string;
|
|
7
7
|
methods: string[];
|
|
@@ -10,6 +10,10 @@ export interface ManifestRoute {
|
|
|
10
10
|
expires?: string;
|
|
11
11
|
generated?: string;
|
|
12
12
|
description?: string;
|
|
13
|
+
/** Execution mode for the route's whole `function`/`middleware` chain: `true` for the
|
|
14
|
+
* QuickJS sandbox, `false` for trusted in-process execution. */
|
|
15
|
+
sandbox: boolean;
|
|
16
|
+
sandboxReason?: string;
|
|
13
17
|
middleware: {
|
|
14
18
|
source: string;
|
|
15
19
|
export: string;
|
|
@@ -75,5 +79,4 @@ export interface Manifest {
|
|
|
75
79
|
export declare function buildManifest(project: string, options?: InspectOptions): Promise<Manifest>;
|
|
76
80
|
/** The manifest as `build` writes it: two-space JSON with a trailing newline. */
|
|
77
81
|
export declare function renderManifest(manifest: Manifest): string;
|
|
78
|
-
export declare const manifestFileName = "manifest.json";
|
|
79
82
|
export declare function manifestPath(out: string): string;
|