@pwfabric/cli 0.0.0-stage → 1.0.0-rc.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/LICENSE +202 -0
- package/README.md +122 -2
- package/dist/args.d.ts +11 -0
- package/dist/args.js +43 -0
- package/dist/args.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +10 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.js +56 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.js +55 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.js +119 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +12 -0
- package/dist/commands/dev.js +107 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/lib/flags.d.ts +10 -0
- package/dist/commands/lib/flags.js +33 -0
- package/dist/commands/lib/flags.js.map +1 -0
- package/dist/commands/lib/identity.d.ts +38 -0
- package/dist/commands/lib/identity.js +120 -0
- package/dist/commands/lib/identity.js.map +1 -0
- package/dist/commands/lib/project.d.ts +34 -0
- package/dist/commands/lib/project.js +78 -0
- package/dist/commands/lib/project.js.map +1 -0
- package/dist/commands/lib/report.d.ts +15 -0
- package/dist/commands/lib/report.js +75 -0
- package/dist/commands/lib/report.js.map +1 -0
- package/dist/commands/lib/starter.d.ts +41 -0
- package/dist/commands/lib/starter.js +136 -0
- package/dist/commands/lib/starter.js.map +1 -0
- package/dist/commands/lib/static-server.d.ts +45 -0
- package/dist/commands/lib/static-server.js +176 -0
- package/dist/commands/lib/static-server.js.map +1 -0
- package/dist/commands/pack.d.ts +5 -0
- package/dist/commands/pack.js +56 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/registry.d.ts +7 -0
- package/dist/commands/registry.js +16 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/run.d.ts +14 -0
- package/dist/commands/run.js +79 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/types.d.ts +34 -0
- package/dist/commands/types.js +10 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/validate.d.ts +6 -0
- package/dist/commands/validate.js +33 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/dev/assemble.d.ts +47 -0
- package/dist/dev/assemble.js +339 -0
- package/dist/dev/assemble.js.map +1 -0
- package/dist/dev/compile.d.ts +54 -0
- package/dist/dev/compile.js +215 -0
- package/dist/dev/compile.js.map +1 -0
- package/dist/dev/csp.d.ts +67 -0
- package/dist/dev/csp.js +260 -0
- package/dist/dev/csp.js.map +1 -0
- package/dist/dev/diagnostics.d.ts +21 -0
- package/dist/dev/diagnostics.js +22 -0
- package/dist/dev/diagnostics.js.map +1 -0
- package/dist/dev/envelope.d.ts +92 -0
- package/dist/dev/envelope.js +154 -0
- package/dist/dev/envelope.js.map +1 -0
- package/dist/dev/media.d.ts +53 -0
- package/dist/dev/media.js +128 -0
- package/dist/dev/media.js.map +1 -0
- package/dist/dev/needs-host.d.ts +2 -0
- package/dist/dev/needs-host.js +32 -0
- package/dist/dev/needs-host.js.map +1 -0
- package/dist/dev/output.d.ts +9 -0
- package/dist/dev/output.js +32 -0
- package/dist/dev/output.js.map +1 -0
- package/dist/dev/overlay.d.ts +21 -0
- package/dist/dev/overlay.js +59 -0
- package/dist/dev/overlay.js.map +1 -0
- package/dist/dev/server.d.ts +27 -0
- package/dist/dev/server.js +268 -0
- package/dist/dev/server.js.map +1 -0
- package/dist/dev/shell.d.ts +94 -0
- package/dist/dev/shell.js +267 -0
- package/dist/dev/shell.js.map +1 -0
- package/dist/dev/sources.d.ts +45 -0
- package/dist/dev/sources.js +165 -0
- package/dist/dev/sources.js.map +1 -0
- package/dist/dev/theme.d.ts +51 -0
- package/dist/dev/theme.js +185 -0
- package/dist/dev/theme.js.map +1 -0
- package/dist/dev/types.d.ts +40 -0
- package/dist/dev/types.js +11 -0
- package/dist/dev/types.js.map +1 -0
- package/dist/dev/watcher.d.ts +29 -0
- package/dist/dev/watcher.js +150 -0
- package/dist/dev/watcher.js.map +1 -0
- package/dist/host/definitions.d.ts +61 -0
- package/dist/host/definitions.js +136 -0
- package/dist/host/definitions.js.map +1 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +156 -0
- package/dist/host/http.js.map +1 -0
- package/dist/host/local-host.d.ts +59 -0
- package/dist/host/local-host.js +420 -0
- package/dist/host/local-host.js.map +1 -0
- package/dist/host/oidc.d.ts +46 -0
- package/dist/host/oidc.js +192 -0
- package/dist/host/oidc.js.map +1 -0
- package/dist/host/query.d.ts +35 -0
- package/dist/host/query.js +158 -0
- package/dist/host/query.js.map +1 -0
- package/dist/host/receipts.d.ts +25 -0
- package/dist/host/receipts.js +72 -0
- package/dist/host/receipts.js.map +1 -0
- package/dist/host/records.d.ts +73 -0
- package/dist/host/records.js +229 -0
- package/dist/host/records.js.map +1 -0
- package/dist/host/tokens.d.ts +28 -0
- package/dist/host/tokens.js +52 -0
- package/dist/host/tokens.js.map +1 -0
- package/dist/host/types.d.ts +32 -0
- package/dist/host/types.js +11 -0
- package/dist/host/types.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -3
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an assembly says about the folder it read: one shape for compiler
|
|
3
|
+
* output, validation findings and the assembler's own checks, so the terminal
|
|
4
|
+
* and the overlay page print the same list.
|
|
5
|
+
*/
|
|
6
|
+
/** `error app/surfaces/home.receipt.json:3:7 UNKNOWN_BLOCK_TYPE …` */
|
|
7
|
+
export function formatDiagnostic(d) {
|
|
8
|
+
const where = d.path ? `${d.path}${d.line !== undefined ? `:${d.line}${d.column !== undefined ? `:${d.column}` : ''}` : ''} ` : '';
|
|
9
|
+
return `${d.severity.padEnd(7)} ${where}${d.code} ${d.message}`;
|
|
10
|
+
}
|
|
11
|
+
export function hasErrors(diagnostics) {
|
|
12
|
+
return diagnostics.some((d) => d.severity === 'error');
|
|
13
|
+
}
|
|
14
|
+
/** Errors first, then warnings, then info; stable within a severity. */
|
|
15
|
+
export function sortDiagnostics(diagnostics) {
|
|
16
|
+
const rank = { error: 0, warning: 1, info: 2 };
|
|
17
|
+
return diagnostics
|
|
18
|
+
.map((d, i) => [d, i])
|
|
19
|
+
.sort((a, b) => rank[a[0].severity] - rank[b[0].severity] || a[1] - b[1])
|
|
20
|
+
.map(([d]) => d);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/dev/diagnostics.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,CAAqB;IACpD,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;IACnI,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAA;AAClE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,WAA0C;IAClE,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAA;AACxD,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAAC,WAA0C;IACxE,MAAM,IAAI,GAAqC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;IAChF,OAAO,WAAW;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAU,CAAC;SAC9B,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;SACxE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAA;AACpB,CAAC","sourcesContent":["/**\n * What an assembly says about the folder it read: one shape for compiler\n * output, validation findings and the assembler's own checks, so the terminal\n * and the overlay page print the same list.\n */\n\nexport type AssembleSeverity = 'error' | 'warning' | 'info'\n\nexport interface AssembleDiagnostic {\n readonly severity: AssembleSeverity\n /** Machine-readable reason, e.g. `PACK_NOT_CACHED` or a compiler code. */\n readonly code: string\n readonly message: string\n /** The file the finding is about, relative to the app folder when it can be. */\n readonly path?: string\n readonly line?: number\n readonly column?: number\n}\n\n/** `error app/surfaces/home.receipt.json:3:7 UNKNOWN_BLOCK_TYPE …` */\nexport function formatDiagnostic(d: AssembleDiagnostic): string {\n const where = d.path ? `${d.path}${d.line !== undefined ? `:${d.line}${d.column !== undefined ? `:${d.column}` : ''}` : ''} ` : ''\n return `${d.severity.padEnd(7)} ${where}${d.code} ${d.message}`\n}\n\nexport function hasErrors(diagnostics: readonly AssembleDiagnostic[]): boolean {\n return diagnostics.some((d) => d.severity === 'error')\n}\n\n/** Errors first, then warnings, then info; stable within a severity. */\nexport function sortDiagnostics(diagnostics: readonly AssembleDiagnostic[]): AssembleDiagnostic[] {\n const rank: Record<AssembleSeverity, number> = { error: 0, warning: 1, info: 2 }\n return diagnostics\n .map((d, i) => [d, i] as const)\n .sort((a, b) => rank[a[0].severity] - rank[b[0].severity] || a[1] - b[1])\n .map(([d]) => d)\n}\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The envelope documents a runner reads (`pwpack/project.json`,
|
|
3
|
+
* `navigation.json`, `connections.json`, the two module manifests), built
|
|
4
|
+
* from the app folder's declaration.
|
|
5
|
+
*/
|
|
6
|
+
import type { AssembleDiagnostic } from './diagnostics.js';
|
|
7
|
+
/** One page of the app: its key, its receipt and what the receipt says. */
|
|
8
|
+
export interface Page {
|
|
9
|
+
readonly key: string;
|
|
10
|
+
/** Path of the receipt in the app folder. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly bytes: Uint8Array;
|
|
13
|
+
readonly receipt: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** The block types a receipt places, walking `blocks` and every `children`. */
|
|
16
|
+
export declare function blockTypesOf(receipt: unknown): string[];
|
|
17
|
+
/** The page's display name: the receipt's title, else the key. */
|
|
18
|
+
export declare function pageLabel(page: Page): string;
|
|
19
|
+
/** Navigation derived from the route table when the app declares none. */
|
|
20
|
+
export declare function deriveNavigation(pages: readonly Page[], entry: string): {
|
|
21
|
+
navbar: {
|
|
22
|
+
links: {
|
|
23
|
+
label: string;
|
|
24
|
+
href: string;
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/** What the app declares about who may enter (`app/definitions/auth.json`), as `project.json` carries it. */
|
|
29
|
+
export declare function identityOf(auth: unknown): {
|
|
30
|
+
required: boolean;
|
|
31
|
+
topology: 'app' | 'external';
|
|
32
|
+
methods?: string[];
|
|
33
|
+
} | undefined;
|
|
34
|
+
export interface ProjectJsonInput {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly title: string;
|
|
37
|
+
readonly entry: string;
|
|
38
|
+
readonly pages: readonly Page[];
|
|
39
|
+
readonly auth?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** `pwpack/project.json`. */
|
|
42
|
+
export declare function buildProjectJson(input: ProjectJsonInput): Record<string, unknown>;
|
|
43
|
+
/** Config keys that smell of a credential. Such a key never reaches a published app. */
|
|
44
|
+
export declare const CREDENTIAL_KEY_RE: RegExp;
|
|
45
|
+
/**
|
|
46
|
+
* `pwpack/connections.json`: the declaration with every credential-like
|
|
47
|
+
* config key removed (each removal is reported). A bare string entry is read
|
|
48
|
+
* as `{ name }`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildConnections(doc: unknown, diagnostics: AssembleDiagnostic[], path?: string): {
|
|
51
|
+
connections: Record<string, unknown>[];
|
|
52
|
+
} | undefined;
|
|
53
|
+
/** One entry of `blocks/manifest.json`. */
|
|
54
|
+
export interface BlockManifestEntry {
|
|
55
|
+
readonly blockType: string;
|
|
56
|
+
readonly moduleUrl: string;
|
|
57
|
+
readonly integrity: string;
|
|
58
|
+
readonly publisher: string;
|
|
59
|
+
readonly publisherVerified: boolean;
|
|
60
|
+
readonly definition: {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** One entry of `engines/manifest.json`. */
|
|
65
|
+
export interface EngineManifestEntry {
|
|
66
|
+
readonly connection: string;
|
|
67
|
+
readonly atomId: string;
|
|
68
|
+
readonly kind: 'connector' | 'provider';
|
|
69
|
+
readonly moduleUrl?: string;
|
|
70
|
+
readonly integrity?: string;
|
|
71
|
+
readonly engine?: unknown;
|
|
72
|
+
}
|
|
73
|
+
/** A file name for a module: `<name>.<first 8 hex of its hash>.mjs`, with anything unsafe replaced. */
|
|
74
|
+
export declare function moduleFileName(name: string, hexHash: string): string;
|
|
75
|
+
/** The publisher an atom is attributed to: the pack id's publisher segment, or the id itself. */
|
|
76
|
+
export declare function publisherOf(packId: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether a local host may write `name` into the envelope: a relative path of
|
|
79
|
+
* `/`-separated segments ending in `.json` (`auth.json`,
|
|
80
|
+
* `surfaces/home.receipt.json`), each segment starting with a letter, digit,
|
|
81
|
+
* `_` or `-` — so no `.`/`..` segment, no hidden file, no absolute path.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isEnvelopeFileName(name: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Merges what a local host adds to the envelope. Each key is a file name
|
|
86
|
+
* relative to `pwpack/` (`auth.json`, `surfaces/<key>.receipt.json`) and each
|
|
87
|
+
* value the document; a value for `project.json` is merged into it member by
|
|
88
|
+
* member. Called after the static envelope is laid out, so what the host
|
|
89
|
+
* offers WINS: its receipts (entity sources rewritten to its record
|
|
90
|
+
* endpoints) replace the folder's copies.
|
|
91
|
+
*/
|
|
92
|
+
export declare function mergeEnvelopeEntries(files: Map<string, Uint8Array>, entries: Readonly<Record<string, unknown>> | undefined, diagnostics: AssembleDiagnostic[]): void;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { isRecord } from './sources.js';
|
|
2
|
+
/** The block types a receipt places, walking `blocks` and every `children`. */
|
|
3
|
+
export function blockTypesOf(receipt) {
|
|
4
|
+
const types = new Set();
|
|
5
|
+
const walk = (node) => {
|
|
6
|
+
if (Array.isArray(node)) {
|
|
7
|
+
node.forEach(walk);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (!isRecord(node))
|
|
11
|
+
return;
|
|
12
|
+
if (typeof node['type'] === 'string' && node['type'])
|
|
13
|
+
types.add(node['type']);
|
|
14
|
+
if (Array.isArray(node['children']))
|
|
15
|
+
node['children'].forEach(walk);
|
|
16
|
+
};
|
|
17
|
+
if (isRecord(receipt))
|
|
18
|
+
walk(receipt['blocks']);
|
|
19
|
+
return [...types];
|
|
20
|
+
}
|
|
21
|
+
/** The page's display name: the receipt's title, else the key. */
|
|
22
|
+
export function pageLabel(page) {
|
|
23
|
+
const title = page.receipt['title'];
|
|
24
|
+
return typeof title === 'string' && title.trim() ? title.trim() : page.key;
|
|
25
|
+
}
|
|
26
|
+
/** Navigation derived from the route table when the app declares none. */
|
|
27
|
+
export function deriveNavigation(pages, entry) {
|
|
28
|
+
return {
|
|
29
|
+
navbar: { links: pages.map((p) => ({ label: pageLabel(p), href: p.key === entry ? '/' : `/${p.key}` })) },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** What the app declares about who may enter (`app/definitions/auth.json`), as `project.json` carries it. */
|
|
33
|
+
export function identityOf(auth) {
|
|
34
|
+
if (!isRecord(auth))
|
|
35
|
+
return undefined;
|
|
36
|
+
const topology = auth['topology'] === 'external' ? 'external' : 'app';
|
|
37
|
+
const methods = Array.isArray(auth['methods']) ? auth['methods'].filter((m) => typeof m === 'string') : [];
|
|
38
|
+
return {
|
|
39
|
+
required: auth['required'] === true,
|
|
40
|
+
topology,
|
|
41
|
+
...(topology === 'app' ? { methods } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** `pwpack/project.json`. */
|
|
45
|
+
export function buildProjectJson(input) {
|
|
46
|
+
const surfaceReceiptFiles = {};
|
|
47
|
+
for (const p of input.pages)
|
|
48
|
+
surfaceReceiptFiles[p.key] = `surfaces/${p.key}.receipt.json`;
|
|
49
|
+
const identity = identityOf(input.auth);
|
|
50
|
+
return {
|
|
51
|
+
schemaVersion: '1.0.0',
|
|
52
|
+
id: input.id,
|
|
53
|
+
title: input.title,
|
|
54
|
+
entrySurfaceId: input.entry,
|
|
55
|
+
surfaceOrder: input.pages.map((p) => p.key),
|
|
56
|
+
surfaceReceiptFiles,
|
|
57
|
+
...(identity ? { identity } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Config keys that smell of a credential. Such a key never reaches a published app. */
|
|
61
|
+
export const CREDENTIAL_KEY_RE = /(auth|header|credential|secret|token|password|passwd|apikey|api_key|clientsecret|client_secret|bearer|cookie|signature)/i;
|
|
62
|
+
function stripCredentials(value, path, dropped) {
|
|
63
|
+
if (Array.isArray(value))
|
|
64
|
+
return value.map((v, i) => stripCredentials(v, `${path}[${i}]`, dropped));
|
|
65
|
+
if (!isRecord(value))
|
|
66
|
+
return value;
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [k, v] of Object.entries(value)) {
|
|
69
|
+
if (CREDENTIAL_KEY_RE.test(k)) {
|
|
70
|
+
dropped.push(`${path}.${k}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
out[k] = stripCredentials(v, `${path}.${k}`, dropped);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `pwpack/connections.json`: the declaration with every credential-like
|
|
79
|
+
* config key removed (each removal is reported). A bare string entry is read
|
|
80
|
+
* as `{ name }`.
|
|
81
|
+
*/
|
|
82
|
+
export function buildConnections(doc, diagnostics, path = 'app/definitions/connections.json') {
|
|
83
|
+
if (!isRecord(doc))
|
|
84
|
+
return undefined;
|
|
85
|
+
const list = Array.isArray(doc['connections']) ? doc['connections'] : [];
|
|
86
|
+
const connections = [];
|
|
87
|
+
for (const raw of list) {
|
|
88
|
+
const entry = typeof raw === 'string' ? { name: raw } : isRecord(raw) ? raw : null;
|
|
89
|
+
if (!entry || typeof entry['name'] !== 'string') {
|
|
90
|
+
diagnostics.push({ severity: 'warning', code: 'CONNECTION_INVALID', message: 'a connection without a name is skipped', path });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const dropped = [];
|
|
94
|
+
const out = { ...entry };
|
|
95
|
+
if (entry['config'] !== undefined)
|
|
96
|
+
out['config'] = stripCredentials(entry['config'], 'config', dropped);
|
|
97
|
+
for (const d of dropped) {
|
|
98
|
+
diagnostics.push({
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
code: 'CREDENTIAL_DROPPED',
|
|
101
|
+
message: `connection "${entry['name']}": ${d} looks like a credential and is not published`,
|
|
102
|
+
path,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
connections.push(out);
|
|
106
|
+
}
|
|
107
|
+
return { connections };
|
|
108
|
+
}
|
|
109
|
+
/** A file name for a module: `<name>.<first 8 hex of its hash>.mjs`, with anything unsafe replaced. */
|
|
110
|
+
export function moduleFileName(name, hexHash) {
|
|
111
|
+
const safe = name.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^\.+/, '_') || 'module';
|
|
112
|
+
return `${safe}.${hexHash.slice(0, 8)}.mjs`;
|
|
113
|
+
}
|
|
114
|
+
/** The publisher an atom is attributed to: the pack id's publisher segment, or the id itself. */
|
|
115
|
+
export function publisherOf(packId) {
|
|
116
|
+
const m = /^@pwpack\/([^/]+)\//.exec(packId);
|
|
117
|
+
return m?.[1] ?? packId;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether a local host may write `name` into the envelope: a relative path of
|
|
121
|
+
* `/`-separated segments ending in `.json` (`auth.json`,
|
|
122
|
+
* `surfaces/home.receipt.json`), each segment starting with a letter, digit,
|
|
123
|
+
* `_` or `-` — so no `.`/`..` segment, no hidden file, no absolute path.
|
|
124
|
+
*/
|
|
125
|
+
export function isEnvelopeFileName(name) {
|
|
126
|
+
return /^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\.json$/.test(name);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Merges what a local host adds to the envelope. Each key is a file name
|
|
130
|
+
* relative to `pwpack/` (`auth.json`, `surfaces/<key>.receipt.json`) and each
|
|
131
|
+
* value the document; a value for `project.json` is merged into it member by
|
|
132
|
+
* member. Called after the static envelope is laid out, so what the host
|
|
133
|
+
* offers WINS: its receipts (entity sources rewritten to its record
|
|
134
|
+
* endpoints) replace the folder's copies.
|
|
135
|
+
*/
|
|
136
|
+
export function mergeEnvelopeEntries(files, entries, diagnostics) {
|
|
137
|
+
if (!entries)
|
|
138
|
+
return;
|
|
139
|
+
const enc = new TextEncoder();
|
|
140
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
141
|
+
if (!isEnvelopeFileName(name)) {
|
|
142
|
+
diagnostics.push({ severity: 'warning', code: 'HOST_ENTRY_IGNORED', message: `the local host offered "${name}", which is not an envelope file name` });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const path = `pwpack/${name}`;
|
|
146
|
+
if (name === 'project.json' && isRecord(value)) {
|
|
147
|
+
const current = JSON.parse(new TextDecoder().decode(files.get(path)));
|
|
148
|
+
files.set(path, enc.encode(JSON.stringify({ ...current, ...value })));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
files.set(path, enc.encode(JSON.stringify(value)));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../src/dev/envelope.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAWvC,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,MAAM,IAAI,GAAG,CAAC,IAAa,EAAQ,EAAE;QACnC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YAClB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAM;QAC3B,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;QAC7E,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACrE,CAAC,CAAA;IACD,IAAI,QAAQ,CAAC,OAAO,CAAC;QAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC9C,OAAO,CAAC,GAAG,KAAK,CAAC,CAAA;AACnB,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAA;AAC5E,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,gBAAgB,CAAC,KAAsB,EAAE,KAAa;IACpE,OAAO;QACL,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE;KAC1G,CAAA;AACH,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAA;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAA;IACrE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACvH,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI;QACnC,QAAQ;QACR,GAAG,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3C,CAAA;AACH,CAAC;AAUD,6BAA6B;AAC7B,MAAM,UAAU,gBAAgB,CAAC,KAAuB;IACtD,MAAM,mBAAmB,GAA2B,EAAE,CAAA;IACtD,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK;QAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,CAAC,GAAG,eAAe,CAAA;IAC1F,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACvC,OAAO;QACL,aAAa,EAAE,OAAO;QACtB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,cAAc,EAAE,KAAK,CAAC,KAAK;QAC3B,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAC3C,mBAAmB;QACnB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClC,CAAA;AACH,CAAC;AAED,wFAAwF;AACxF,MAAM,CAAC,MAAM,iBAAiB,GAC5B,0HAA0H,CAAA;AAE5H,SAAS,gBAAgB,CAAC,KAAc,EAAE,IAAY,EAAE,OAAiB;IACvE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAA;IACnG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAClC,MAAM,GAAG,GAA4B,EAAE,CAAA;IACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAA;YAC5B,SAAQ;QACV,CAAC;QACD,GAAG,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAY,EACZ,WAAiC,EACjC,IAAI,GAAG,kCAAkC;IAEzC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACxE,MAAM,WAAW,GAA8B,EAAE,CAAA;IACjD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;QAClF,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;YAChD,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,wCAAwC,EAAE,IAAI,EAAE,CAAC,CAAA;YAC9H,SAAQ;QACV,CAAC;QACD,MAAM,OAAO,GAAa,EAAE,CAAA;QAC5B,MAAM,GAAG,GAA4B,EAAE,GAAG,KAAK,EAAE,CAAA;QACjD,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,SAAS;YAAE,GAAG,CAAC,QAAQ,CAAC,GAAG,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;QACvG,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,WAAW,CAAC,IAAI,CAAC;gBACf,QAAQ,EAAE,SAAS;gBACnB,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,eAAe,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,+CAA+C;gBAC3F,IAAI;aACL,CAAC,CAAA;QACJ,CAAC;QACD,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,CAAA;AACxB,CAAC;AAsBD,uGAAuG;AACvG,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,OAAe;IAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,QAAQ,CAAA;IACpF,OAAO,GAAG,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAA;AAC7C,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC5C,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,MAAM,CAAA;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,yEAAyE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAC7F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAA8B,EAC9B,OAAsD,EACtD,WAAiC;IAEjC,IAAI,CAAC,OAAO;QAAE,OAAM;IACpB,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAA;IAC7B,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,2BAA2B,IAAI,uCAAuC,EAAE,CAAC,CAAA;YACtJ,SAAQ;QACV,CAAC;QACD,MAAM,IAAI,GAAG,UAAU,IAAI,EAAE,CAAA;QAC7B,IAAI,IAAI,KAAK,cAAc,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAe,CAAC,CAA4B,CAAA;YAC9G,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;YACrE,SAAQ;QACV,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACpD,CAAC;AACH,CAAC","sourcesContent":["/**\n * The envelope documents a runner reads (`pwpack/project.json`,\n * `navigation.json`, `connections.json`, the two module manifests), built\n * from the app folder's declaration.\n */\nimport type { AssembleDiagnostic } from './diagnostics.js'\nimport { isRecord } from './sources.js'\n\n/** One page of the app: its key, its receipt and what the receipt says. */\nexport interface Page {\n readonly key: string\n /** Path of the receipt in the app folder. */\n readonly path: string\n readonly bytes: Uint8Array\n readonly receipt: Record<string, unknown>\n}\n\n/** The block types a receipt places, walking `blocks` and every `children`. */\nexport function blockTypesOf(receipt: unknown): string[] {\n const types = new Set<string>()\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n node.forEach(walk)\n return\n }\n if (!isRecord(node)) return\n if (typeof node['type'] === 'string' && node['type']) types.add(node['type'])\n if (Array.isArray(node['children'])) node['children'].forEach(walk)\n }\n if (isRecord(receipt)) walk(receipt['blocks'])\n return [...types]\n}\n\n/** The page's display name: the receipt's title, else the key. */\nexport function pageLabel(page: Page): string {\n const title = page.receipt['title']\n return typeof title === 'string' && title.trim() ? title.trim() : page.key\n}\n\n/** Navigation derived from the route table when the app declares none. */\nexport function deriveNavigation(pages: readonly Page[], entry: string): { navbar: { links: { label: string; href: string }[] } } {\n return {\n navbar: { links: pages.map((p) => ({ label: pageLabel(p), href: p.key === entry ? '/' : `/${p.key}` })) },\n }\n}\n\n/** What the app declares about who may enter (`app/definitions/auth.json`), as `project.json` carries it. */\nexport function identityOf(auth: unknown): { required: boolean; topology: 'app' | 'external'; methods?: string[] } | undefined {\n if (!isRecord(auth)) return undefined\n const topology = auth['topology'] === 'external' ? 'external' : 'app'\n const methods = Array.isArray(auth['methods']) ? auth['methods'].filter((m): m is string => typeof m === 'string') : []\n return {\n required: auth['required'] === true,\n topology,\n ...(topology === 'app' ? { methods } : {}),\n }\n}\n\nexport interface ProjectJsonInput {\n readonly id: string\n readonly title: string\n readonly entry: string\n readonly pages: readonly Page[]\n readonly auth?: unknown\n}\n\n/** `pwpack/project.json`. */\nexport function buildProjectJson(input: ProjectJsonInput): Record<string, unknown> {\n const surfaceReceiptFiles: Record<string, string> = {}\n for (const p of input.pages) surfaceReceiptFiles[p.key] = `surfaces/${p.key}.receipt.json`\n const identity = identityOf(input.auth)\n return {\n schemaVersion: '1.0.0',\n id: input.id,\n title: input.title,\n entrySurfaceId: input.entry,\n surfaceOrder: input.pages.map((p) => p.key),\n surfaceReceiptFiles,\n ...(identity ? { identity } : {}),\n }\n}\n\n/** Config keys that smell of a credential. Such a key never reaches a published app. */\nexport const CREDENTIAL_KEY_RE =\n /(auth|header|credential|secret|token|password|passwd|apikey|api_key|clientsecret|client_secret|bearer|cookie|signature)/i\n\nfunction stripCredentials(value: unknown, path: string, dropped: string[]): unknown {\n if (Array.isArray(value)) return value.map((v, i) => stripCredentials(v, `${path}[${i}]`, dropped))\n if (!isRecord(value)) return value\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value)) {\n if (CREDENTIAL_KEY_RE.test(k)) {\n dropped.push(`${path}.${k}`)\n continue\n }\n out[k] = stripCredentials(v, `${path}.${k}`, dropped)\n }\n return out\n}\n\n/**\n * `pwpack/connections.json`: the declaration with every credential-like\n * config key removed (each removal is reported). A bare string entry is read\n * as `{ name }`.\n */\nexport function buildConnections(\n doc: unknown,\n diagnostics: AssembleDiagnostic[],\n path = 'app/definitions/connections.json'\n): { connections: Record<string, unknown>[] } | undefined {\n if (!isRecord(doc)) return undefined\n const list = Array.isArray(doc['connections']) ? doc['connections'] : []\n const connections: Record<string, unknown>[] = []\n for (const raw of list) {\n const entry = typeof raw === 'string' ? { name: raw } : isRecord(raw) ? raw : null\n if (!entry || typeof entry['name'] !== 'string') {\n diagnostics.push({ severity: 'warning', code: 'CONNECTION_INVALID', message: 'a connection without a name is skipped', path })\n continue\n }\n const dropped: string[] = []\n const out: Record<string, unknown> = { ...entry }\n if (entry['config'] !== undefined) out['config'] = stripCredentials(entry['config'], 'config', dropped)\n for (const d of dropped) {\n diagnostics.push({\n severity: 'warning',\n code: 'CREDENTIAL_DROPPED',\n message: `connection \"${entry['name']}\": ${d} looks like a credential and is not published`,\n path,\n })\n }\n connections.push(out)\n }\n return { connections }\n}\n\n/** One entry of `blocks/manifest.json`. */\nexport interface BlockManifestEntry {\n readonly blockType: string\n readonly moduleUrl: string\n readonly integrity: string\n readonly publisher: string\n readonly publisherVerified: boolean\n readonly definition: { readonly name: string }\n}\n\n/** One entry of `engines/manifest.json`. */\nexport interface EngineManifestEntry {\n readonly connection: string\n readonly atomId: string\n readonly kind: 'connector' | 'provider'\n readonly moduleUrl?: string\n readonly integrity?: string\n readonly engine?: unknown\n}\n\n/** A file name for a module: `<name>.<first 8 hex of its hash>.mjs`, with anything unsafe replaced. */\nexport function moduleFileName(name: string, hexHash: string): string {\n const safe = name.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^\\.+/, '_') || 'module'\n return `${safe}.${hexHash.slice(0, 8)}.mjs`\n}\n\n/** The publisher an atom is attributed to: the pack id's publisher segment, or the id itself. */\nexport function publisherOf(packId: string): string {\n const m = /^@pwpack\\/([^/]+)\\//.exec(packId)\n return m?.[1] ?? packId\n}\n\n/**\n * Whether a local host may write `name` into the envelope: a relative path of\n * `/`-separated segments ending in `.json` (`auth.json`,\n * `surfaces/home.receipt.json`), each segment starting with a letter, digit,\n * `_` or `-` — so no `.`/`..` segment, no hidden file, no absolute path.\n */\nexport function isEnvelopeFileName(name: string): boolean {\n return /^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*\\.json$/.test(name)\n}\n\n/**\n * Merges what a local host adds to the envelope. Each key is a file name\n * relative to `pwpack/` (`auth.json`, `surfaces/<key>.receipt.json`) and each\n * value the document; a value for `project.json` is merged into it member by\n * member. Called after the static envelope is laid out, so what the host\n * offers WINS: its receipts (entity sources rewritten to its record\n * endpoints) replace the folder's copies.\n */\nexport function mergeEnvelopeEntries(\n files: Map<string, Uint8Array>,\n entries: Readonly<Record<string, unknown>> | undefined,\n diagnostics: AssembleDiagnostic[]\n): void {\n if (!entries) return\n const enc = new TextEncoder()\n for (const [name, value] of Object.entries(entries)) {\n if (!isEnvelopeFileName(name)) {\n diagnostics.push({ severity: 'warning', code: 'HOST_ENTRY_IGNORED', message: `the local host offered \"${name}\", which is not an envelope file name` })\n continue\n }\n const path = `pwpack/${name}`\n if (name === 'project.json' && isRecord(value)) {\n const current = JSON.parse(new TextDecoder().decode(files.get(path) as Uint8Array)) as Record<string, unknown>\n files.set(path, enc.encode(JSON.stringify({ ...current, ...value })))\n continue\n }\n files.set(path, enc.encode(JSON.stringify(value)))\n }\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media: the app's declared images and files, copied into the pwapp.
|
|
3
|
+
*
|
|
4
|
+
* pwpack-format §1.3: `app/media/<key>.json` declares one image or file
|
|
5
|
+
* (`{ purpose, kind, origin, slots, status, url?, decorative?, file? }`) and
|
|
6
|
+
* carries no bytes; the bytes are `media/<file>`, each recorded in
|
|
7
|
+
* `media/provenance.json`. The pack library's `mediaDeclarations` reads
|
|
8
|
+
* that layout (its findings reach the diagnostics through `validatePack`).
|
|
9
|
+
* pwapp-format §1 puts the bytes the app serves at `pwpack/media/<file>`,
|
|
10
|
+
* and §4 records each declaration's resolved address in `publish.json`
|
|
11
|
+
* `media[]`.
|
|
12
|
+
*
|
|
13
|
+
* For each declaration the bytes are looked up, in order:
|
|
14
|
+
*
|
|
15
|
+
* 1. the declaration's `file` (a pack path, `media/<name>`);
|
|
16
|
+
* 2. `media/<path>` — a `url` that is itself a `media/…` path;
|
|
17
|
+
* 3. `media/<key>.<ext>` — the one media file named after the key.
|
|
18
|
+
*
|
|
19
|
+
* Found bytes are served at `pwpack/media/…` and the declaration resolves to
|
|
20
|
+
* that address. A declaration without bytes but with an absolute `url` keeps
|
|
21
|
+
* the url. Every other one is reported (info) and left alone.
|
|
22
|
+
*
|
|
23
|
+
* Slots: a declared slot (`{ surfaceKey, blockId, prop }`) whose prop is
|
|
24
|
+
* empty in the served receipt gets the resolved address. A value already in
|
|
25
|
+
* the prop is never replaced (plan.md §2 rule 1). The app's own files are
|
|
26
|
+
* never rewritten; only the served copy of the receipt is.
|
|
27
|
+
*/
|
|
28
|
+
import { type PackFolder } from '@pwfabric/pack';
|
|
29
|
+
import type { AssembleDiagnostic } from './diagnostics.js';
|
|
30
|
+
/** One slot a resolved address is written into. */
|
|
31
|
+
export interface MediaSlot {
|
|
32
|
+
readonly blockId: string;
|
|
33
|
+
readonly prop: string;
|
|
34
|
+
readonly url: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CollectedMedia {
|
|
37
|
+
/** Files to serve, relative to `pwpack/` (`media/hero.webp`). */
|
|
38
|
+
readonly files: Map<string, Uint8Array>;
|
|
39
|
+
/** `publish.json` `media[]`, in key order. */
|
|
40
|
+
readonly records: {
|
|
41
|
+
key: string;
|
|
42
|
+
url: string;
|
|
43
|
+
}[];
|
|
44
|
+
/** Slots to fill, by page key. */
|
|
45
|
+
readonly slots: Map<string, MediaSlot[]>;
|
|
46
|
+
}
|
|
47
|
+
/** Reads the app's media declarations and bytes. */
|
|
48
|
+
export declare function collectMedia(folder: PackFolder, diagnostics: AssembleDiagnostic[]): CollectedMedia;
|
|
49
|
+
/**
|
|
50
|
+
* The served receipt with its empty media slots filled, or `undefined` when
|
|
51
|
+
* nothing changed (the bytes are then served exactly as written).
|
|
52
|
+
*/
|
|
53
|
+
export declare function fillMediaSlots(receiptBytes: Uint8Array, slots: readonly MediaSlot[] | undefined): Uint8Array | undefined;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media: the app's declared images and files, copied into the pwapp.
|
|
3
|
+
*
|
|
4
|
+
* pwpack-format §1.3: `app/media/<key>.json` declares one image or file
|
|
5
|
+
* (`{ purpose, kind, origin, slots, status, url?, decorative?, file? }`) and
|
|
6
|
+
* carries no bytes; the bytes are `media/<file>`, each recorded in
|
|
7
|
+
* `media/provenance.json`. The pack library's `mediaDeclarations` reads
|
|
8
|
+
* that layout (its findings reach the diagnostics through `validatePack`).
|
|
9
|
+
* pwapp-format §1 puts the bytes the app serves at `pwpack/media/<file>`,
|
|
10
|
+
* and §4 records each declaration's resolved address in `publish.json`
|
|
11
|
+
* `media[]`.
|
|
12
|
+
*
|
|
13
|
+
* For each declaration the bytes are looked up, in order:
|
|
14
|
+
*
|
|
15
|
+
* 1. the declaration's `file` (a pack path, `media/<name>`);
|
|
16
|
+
* 2. `media/<path>` — a `url` that is itself a `media/…` path;
|
|
17
|
+
* 3. `media/<key>.<ext>` — the one media file named after the key.
|
|
18
|
+
*
|
|
19
|
+
* Found bytes are served at `pwpack/media/…` and the declaration resolves to
|
|
20
|
+
* that address. A declaration without bytes but with an absolute `url` keeps
|
|
21
|
+
* the url. Every other one is reported (info) and left alone.
|
|
22
|
+
*
|
|
23
|
+
* Slots: a declared slot (`{ surfaceKey, blockId, prop }`) whose prop is
|
|
24
|
+
* empty in the served receipt gets the resolved address. A value already in
|
|
25
|
+
* the prop is never replaced (plan.md §2 rule 1). The app's own files are
|
|
26
|
+
* never rewritten; only the served copy of the receipt is.
|
|
27
|
+
*/
|
|
28
|
+
import { MEDIA_DIR, MEDIA_PROVENANCE_FILE, mediaDeclarations } from '@pwfabric/pack';
|
|
29
|
+
import { isRecord } from './sources.js';
|
|
30
|
+
const ABSOLUTE_URL = /^(?:https?:|data:)/i;
|
|
31
|
+
/** A single path segment that is safe to serve: no separators, no dot names. */
|
|
32
|
+
function isSafeName(name) {
|
|
33
|
+
return name.length > 0 && !/[\\/\0]/.test(name) && name !== '.' && name !== '..';
|
|
34
|
+
}
|
|
35
|
+
/** A relative `media/…` path with no `.`/`..` segment. */
|
|
36
|
+
function isSafeMediaPath(path) {
|
|
37
|
+
return path.startsWith(MEDIA_DIR) && path.slice(MEDIA_DIR.length).split('/').every(isSafeName);
|
|
38
|
+
}
|
|
39
|
+
/** The media file named after a key (`media/<key>.<ext>`), when exactly one exists. */
|
|
40
|
+
function byKey(mediaPaths, key) {
|
|
41
|
+
const hits = mediaPaths.filter((p) => {
|
|
42
|
+
const name = p.slice(MEDIA_DIR.length);
|
|
43
|
+
return !name.includes('/') && name.slice(0, name.lastIndexOf('.') > 0 ? name.lastIndexOf('.') : undefined) === key;
|
|
44
|
+
});
|
|
45
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Reads the app's media declarations and bytes. */
|
|
48
|
+
export function collectMedia(folder, diagnostics) {
|
|
49
|
+
const files = new Map();
|
|
50
|
+
const records = [];
|
|
51
|
+
const slots = new Map();
|
|
52
|
+
const mediaPaths = [...folder.files.keys()].filter((p) => p !== MEDIA_PROVENANCE_FILE && isSafeMediaPath(p)).sort();
|
|
53
|
+
for (const path of mediaPaths)
|
|
54
|
+
files.set(path, folder.files.get(path));
|
|
55
|
+
for (const { key, path, declaration, filePath } of mediaDeclarations(folder).declarations) {
|
|
56
|
+
const declaredUrl = declaration.url?.trim() ?? '';
|
|
57
|
+
const urlPath = declaredUrl.replace(/^\.\//, '');
|
|
58
|
+
let url;
|
|
59
|
+
const inMedia = (filePath !== undefined && files.has(filePath) ? filePath : undefined) ??
|
|
60
|
+
(isSafeMediaPath(urlPath) && files.has(urlPath) ? urlPath : undefined) ??
|
|
61
|
+
byKey(mediaPaths, key);
|
|
62
|
+
if (inMedia) {
|
|
63
|
+
url = `pwpack/${inMedia}`;
|
|
64
|
+
}
|
|
65
|
+
else if (ABSOLUTE_URL.test(declaredUrl)) {
|
|
66
|
+
url = declaredUrl;
|
|
67
|
+
}
|
|
68
|
+
if (url === undefined) {
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
severity: 'info',
|
|
71
|
+
code: 'MEDIA_UNRESOLVED',
|
|
72
|
+
message: `media "${key}" has no bytes in media/ and no absolute url; its slots keep what the receipts say`,
|
|
73
|
+
path,
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
records.push({ key, url });
|
|
78
|
+
const served = ABSOLUTE_URL.test(url) ? url : `./${url}`;
|
|
79
|
+
for (const { surfaceKey, blockId, prop } of declaration.slots ?? []) {
|
|
80
|
+
const list = slots.get(surfaceKey) ?? [];
|
|
81
|
+
list.push({ blockId, prop, url: served });
|
|
82
|
+
slots.set(surfaceKey, list);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { files, records, slots };
|
|
86
|
+
}
|
|
87
|
+
const isEmpty = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
|
|
88
|
+
/**
|
|
89
|
+
* The served receipt with its empty media slots filled, or `undefined` when
|
|
90
|
+
* nothing changed (the bytes are then served exactly as written).
|
|
91
|
+
*/
|
|
92
|
+
export function fillMediaSlots(receiptBytes, slots) {
|
|
93
|
+
if (!slots || slots.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
let receipt;
|
|
96
|
+
try {
|
|
97
|
+
receipt = JSON.parse(new TextDecoder().decode(receiptBytes));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
if (!isRecord(receipt))
|
|
103
|
+
return undefined;
|
|
104
|
+
const copy = structuredClone(receipt);
|
|
105
|
+
let changed = false;
|
|
106
|
+
const walk = (node) => {
|
|
107
|
+
if (Array.isArray(node)) {
|
|
108
|
+
node.forEach(walk);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!isRecord(node))
|
|
112
|
+
return;
|
|
113
|
+
for (const slot of slots) {
|
|
114
|
+
if (node['id'] !== slot.blockId)
|
|
115
|
+
continue;
|
|
116
|
+
const props = isRecord(node['props']) ? node['props'] : {};
|
|
117
|
+
if (!isEmpty(props[slot.prop]))
|
|
118
|
+
continue;
|
|
119
|
+
node['props'] = { ...props, [slot.prop]: slot.url };
|
|
120
|
+
changed = true;
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(node['children']))
|
|
123
|
+
node['children'].forEach(walk);
|
|
124
|
+
};
|
|
125
|
+
walk(copy['blocks']);
|
|
126
|
+
return changed ? new TextEncoder().encode(JSON.stringify(copy)) : undefined;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../src/dev/media.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Media: the app's declared images and files, copied into the pwapp.\n *\n * pwpack-format §1.3: `app/media/<key>.json` declares one image or file\n * (`{ purpose, kind, origin, slots, status, url?, decorative?, file? }`) and\n * carries no bytes; the bytes are `media/<file>`, each recorded in\n * `media/provenance.json`. The pack library's `mediaDeclarations` reads\n * that layout (its findings reach the diagnostics through `validatePack`).\n * pwapp-format §1 puts the bytes the app serves at `pwpack/media/<file>`,\n * and §4 records each declaration's resolved address in `publish.json`\n * `media[]`.\n *\n * For each declaration the bytes are looked up, in order:\n *\n * 1. the declaration's `file` (a pack path, `media/<name>`);\n * 2. `media/<path>` — a `url` that is itself a `media/…` path;\n * 3. `media/<key>.<ext>` — the one media file named after the key.\n *\n * Found bytes are served at `pwpack/media/…` and the declaration resolves to\n * that address. A declaration without bytes but with an absolute `url` keeps\n * the url. Every other one is reported (info) and left alone.\n *\n * Slots: a declared slot (`{ surfaceKey, blockId, prop }`) whose prop is\n * empty in the served receipt gets the resolved address. A value already in\n * the prop is never replaced (plan.md §2 rule 1). The app's own files are\n * never rewritten; only the served copy of the receipt is.\n */\nimport { MEDIA_DIR, MEDIA_PROVENANCE_FILE, mediaDeclarations, type PackFolder } from '@pwfabric/pack'\nimport type { AssembleDiagnostic } from './diagnostics.js'\nimport { isRecord } from './sources.js'\n\n/** One slot a resolved address is written into. */\nexport interface MediaSlot {\n readonly blockId: string\n readonly prop: string\n readonly url: string\n}\n\nexport interface CollectedMedia {\n /** Files to serve, relative to `pwpack/` (`media/hero.webp`). */\n readonly files: Map<string, Uint8Array>\n /** `publish.json` `media[]`, in key order. */\n readonly records: { key: string; url: string }[]\n /** Slots to fill, by page key. */\n readonly slots: Map<string, MediaSlot[]>\n}\n\nconst ABSOLUTE_URL = /^(?:https?:|data:)/i\n\n/** A single path segment that is safe to serve: no separators, no dot names. */\nfunction isSafeName(name: string): boolean {\n return name.length > 0 && !/[\\\\/\\0]/.test(name) && name !== '.' && name !== '..'\n}\n\n/** A relative `media/…` path with no `.`/`..` segment. */\nfunction isSafeMediaPath(path: string): boolean {\n return path.startsWith(MEDIA_DIR) && path.slice(MEDIA_DIR.length).split('/').every(isSafeName)\n}\n\n/** The media file named after a key (`media/<key>.<ext>`), when exactly one exists. */\nfunction byKey(mediaPaths: readonly string[], key: string): string | undefined {\n const hits = mediaPaths.filter((p) => {\n const name = p.slice(MEDIA_DIR.length)\n return !name.includes('/') && name.slice(0, name.lastIndexOf('.') > 0 ? name.lastIndexOf('.') : undefined) === key\n })\n return hits.length === 1 ? hits[0] : undefined\n}\n\n/** Reads the app's media declarations and bytes. */\nexport function collectMedia(folder: PackFolder, diagnostics: AssembleDiagnostic[]): CollectedMedia {\n const files = new Map<string, Uint8Array>()\n const records: { key: string; url: string }[] = []\n const slots = new Map<string, MediaSlot[]>()\n\n const mediaPaths = [...folder.files.keys()].filter((p) => p !== MEDIA_PROVENANCE_FILE && isSafeMediaPath(p)).sort()\n for (const path of mediaPaths) files.set(path, folder.files.get(path) as Uint8Array)\n\n for (const { key, path, declaration, filePath } of mediaDeclarations(folder).declarations) {\n const declaredUrl = declaration.url?.trim() ?? ''\n const urlPath = declaredUrl.replace(/^\\.\\//, '')\n\n let url: string | undefined\n const inMedia =\n (filePath !== undefined && files.has(filePath) ? filePath : undefined) ??\n (isSafeMediaPath(urlPath) && files.has(urlPath) ? urlPath : undefined) ??\n byKey(mediaPaths, key)\n if (inMedia) {\n url = `pwpack/${inMedia}`\n } else if (ABSOLUTE_URL.test(declaredUrl)) {\n url = declaredUrl\n }\n if (url === undefined) {\n diagnostics.push({\n severity: 'info',\n code: 'MEDIA_UNRESOLVED',\n message: `media \"${key}\" has no bytes in media/ and no absolute url; its slots keep what the receipts say`,\n path,\n })\n continue\n }\n records.push({ key, url })\n const served = ABSOLUTE_URL.test(url) ? url : `./${url}`\n for (const { surfaceKey, blockId, prop } of declaration.slots ?? []) {\n const list = slots.get(surfaceKey) ?? []\n list.push({ blockId, prop, url: served })\n slots.set(surfaceKey, list)\n }\n }\n return { files, records, slots }\n}\n\nconst isEmpty = (v: unknown): boolean => v === undefined || v === null || (typeof v === 'string' && v.trim() === '')\n\n/**\n * The served receipt with its empty media slots filled, or `undefined` when\n * nothing changed (the bytes are then served exactly as written).\n */\nexport function fillMediaSlots(receiptBytes: Uint8Array, slots: readonly MediaSlot[] | undefined): Uint8Array | undefined {\n if (!slots || slots.length === 0) return undefined\n let receipt: unknown\n try {\n receipt = JSON.parse(new TextDecoder().decode(receiptBytes))\n } catch {\n return undefined\n }\n if (!isRecord(receipt)) return undefined\n const copy = structuredClone(receipt)\n let changed = false\n const walk = (node: unknown): void => {\n if (Array.isArray(node)) {\n node.forEach(walk)\n return\n }\n if (!isRecord(node)) return\n for (const slot of slots) {\n if (node['id'] !== slot.blockId) continue\n const props = isRecord(node['props']) ? node['props'] : {}\n if (!isEmpty(props[slot.prop])) continue\n node['props'] = { ...props, [slot.prop]: slot.url }\n changed = true\n }\n if (Array.isArray(node['children'])) node['children'].forEach(walk)\n }\n walk(copy['blocks'])\n return changed ? new TextEncoder().encode(JSON.stringify(copy)) : undefined\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an app needs a local host: it declares sign-in
|
|
3
|
+
* (`app/definitions/auth.json`) or records (`app/definitions/entities.json`).
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { isRecord } from './sources.js';
|
|
8
|
+
async function readJson(path) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse((await readFile(path, 'utf8')).replace(/^\uFEFF/, ''));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** What the app declares that only a host can serve. Empty when it needs none. */
|
|
17
|
+
export async function hostNeeds(root) {
|
|
18
|
+
const needs = [];
|
|
19
|
+
const auth = await readJson(join(root, 'app', 'definitions', 'auth.json'));
|
|
20
|
+
if (isRecord(auth) &&
|
|
21
|
+
(auth['required'] === true ||
|
|
22
|
+
auth['topology'] === 'external' ||
|
|
23
|
+
(Array.isArray(auth['methods']) && auth['methods'].length > 0))) {
|
|
24
|
+
needs.push('sign-in');
|
|
25
|
+
}
|
|
26
|
+
const entities = await readJson(join(root, 'app', 'definitions', 'entities.json'));
|
|
27
|
+
if (isRecord(entities) && Array.isArray(entities['entities']) && entities['entities'].length > 0) {
|
|
28
|
+
needs.push('records');
|
|
29
|
+
}
|
|
30
|
+
return needs;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=needs-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"needs-host.js","sourceRoot":"","sources":["../../src/dev/needs-host.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEvC,KAAK,UAAU,QAAQ,CAAC,IAAY;IAClC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAY,CAAA;IACrF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;IAC1E,IACE,QAAQ,CAAC,IAAI,CAAC;QACd,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI;YACxB,IAAI,CAAC,UAAU,CAAC,KAAK,UAAU;YAC/B,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EACjE,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACvB,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,eAAe,CAAC,CAAC,CAAA;IAClF,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["/**\n * Whether an app needs a local host: it declares sign-in\n * (`app/definitions/auth.json`) or records (`app/definitions/entities.json`).\n */\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { isRecord } from './sources.js'\n\nasync function readJson(path: string): Promise<unknown> {\n try {\n return JSON.parse((await readFile(path, 'utf8')).replace(/^\\uFEFF/, '')) as unknown\n } catch {\n return undefined\n }\n}\n\n/** What the app declares that only a host can serve. Empty when it needs none. */\nexport async function hostNeeds(root: string): Promise<string[]> {\n const needs: string[] = []\n const auth = await readJson(join(root, 'app', 'definitions', 'auth.json'))\n if (\n isRecord(auth) &&\n (auth['required'] === true ||\n auth['topology'] === 'external' ||\n (Array.isArray(auth['methods']) && auth['methods'].length > 0))\n ) {\n needs.push('sign-in')\n }\n const entities = await readJson(join(root, 'app', 'definitions', 'entities.json'))\n if (isRecord(entities) && Array.isArray(entities['entities']) && entities['entities'].length > 0) {\n needs.push('records')\n }\n return needs\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Thrown when the output folder holds something that is not a previous build. */
|
|
2
|
+
export declare class OutputFolderError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Writes `files` into `outDir`, replacing a previous build there. A folder
|
|
6
|
+
* that exists, is not empty and holds no `publish.json` is not a build this
|
|
7
|
+
* command wrote, and is left alone.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writePwapp(files: ReadonlyMap<string, Uint8Array>, outDir: string): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing an assembled pwapp to disk.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
/** Thrown when the output folder holds something that is not a previous build. */
|
|
7
|
+
export class OutputFolderError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Writes `files` into `outDir`, replacing a previous build there. A folder
|
|
11
|
+
* that exists, is not empty and holds no `publish.json` is not a build this
|
|
12
|
+
* command wrote, and is left alone.
|
|
13
|
+
*/
|
|
14
|
+
export async function writePwapp(files, outDir) {
|
|
15
|
+
const st = await stat(outDir).catch(() => null);
|
|
16
|
+
if (st) {
|
|
17
|
+
if (!st.isDirectory())
|
|
18
|
+
throw new OutputFolderError(`${outDir} exists and is not a folder`);
|
|
19
|
+
const entries = await readdir(outDir);
|
|
20
|
+
if (entries.length > 0 && !entries.includes('publish.json')) {
|
|
21
|
+
throw new OutputFolderError(`${outDir} is not empty and holds no previous build (no publish.json); choose another --out`);
|
|
22
|
+
}
|
|
23
|
+
await rm(outDir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
await mkdir(outDir, { recursive: true });
|
|
26
|
+
for (const [path, bytes] of files) {
|
|
27
|
+
const target = join(outDir, ...path.split('/'));
|
|
28
|
+
await mkdir(dirname(target), { recursive: true });
|
|
29
|
+
await writeFile(target, bytes);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/dev/output.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACtE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC,kFAAkF;AAClF,MAAM,OAAO,iBAAkB,SAAQ,KAAK;CAAG;AAE/C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAAsC,EAAE,MAAc;IACrF,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;IAC/C,IAAI,EAAE,EAAE,CAAC;QACP,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE;YAAE,MAAM,IAAI,iBAAiB,CAAC,GAAG,MAAM,6BAA6B,CAAC,CAAA;QAC1F,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;QACrC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YAC5D,MAAM,IAAI,iBAAiB,CAAC,GAAG,MAAM,mFAAmF,CAAC,CAAA;QAC3H,CAAC;QACD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACpD,CAAC;IACD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;QAC/C,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACjD,MAAM,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;IAChC,CAAC;AACH,CAAC","sourcesContent":["/**\n * Writing an assembled pwapp to disk.\n */\nimport { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\n/** Thrown when the output folder holds something that is not a previous build. */\nexport class OutputFolderError extends Error {}\n\n/**\n * Writes `files` into `outDir`, replacing a previous build there. A folder\n * that exists, is not empty and holds no `publish.json` is not a build this\n * command wrote, and is left alone.\n */\nexport async function writePwapp(files: ReadonlyMap<string, Uint8Array>, outDir: string): Promise<void> {\n const st = await stat(outDir).catch(() => null)\n if (st) {\n if (!st.isDirectory()) throw new OutputFolderError(`${outDir} exists and is not a folder`)\n const entries = await readdir(outDir)\n if (entries.length > 0 && !entries.includes('publish.json')) {\n throw new OutputFolderError(`${outDir} is not empty and holds no previous build (no publish.json); choose another --out`)\n }\n await rm(outDir, { recursive: true, force: true })\n }\n await mkdir(outDir, { recursive: true })\n for (const [path, bytes] of files) {\n const target = join(outDir, ...path.split('/'))\n await mkdir(dirname(target), { recursive: true })\n await writeFile(target, bytes)\n }\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `pwf dev` adds to a page: the reload client, and the overlay shown
|
|
3
|
+
* instead of the app when the last build failed.
|
|
4
|
+
*/
|
|
5
|
+
import type { AssembleDiagnostic } from './diagnostics.js';
|
|
6
|
+
/** Server-Sent Events endpoint the page listens on. */
|
|
7
|
+
export declare const EVENTS_PATH = "/_pwf/events";
|
|
8
|
+
/** The current build's status as JSON. */
|
|
9
|
+
export declare const STATUS_PATH = "/_pwf/status";
|
|
10
|
+
/** Where the dev server serves the reload client's script. */
|
|
11
|
+
export declare const RELOAD_PATH = "/_pwf/reload.js";
|
|
12
|
+
/** The reload client's script: reloads the page when the server announces a new build. */
|
|
13
|
+
export declare const RELOAD_JS = "(function () {\n if (typeof EventSource === 'undefined') return\n var es = new EventSource('/_pwf/events')\n es.addEventListener('reload', function () { location.reload() })\n})()\n";
|
|
14
|
+
/**
|
|
15
|
+
* The reload client, as markup for a page. A script file on the dev server's
|
|
16
|
+
* own origin, not an inline script, so the app's Content Security Policy
|
|
17
|
+
* covers it with 'self' (and its event stream with connect-src 'self').
|
|
18
|
+
*/
|
|
19
|
+
export declare const RELOAD_CLIENT = "\n <script src=\"/_pwf/reload.js\"></script>";
|
|
20
|
+
/** The page shown while the last build has errors. */
|
|
21
|
+
export declare function overlayHtml(diagnostics: readonly AssembleDiagnostic[], title?: string): string;
|