@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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +122 -2
  3. package/dist/args.d.ts +11 -0
  4. package/dist/args.js +43 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/bin.d.ts +2 -0
  7. package/dist/bin.js +10 -0
  8. package/dist/bin.js.map +1 -0
  9. package/dist/commands/add.d.ts +3 -0
  10. package/dist/commands/add.js +56 -0
  11. package/dist/commands/add.js.map +1 -0
  12. package/dist/commands/build.d.ts +3 -0
  13. package/dist/commands/build.js +55 -0
  14. package/dist/commands/build.js.map +1 -0
  15. package/dist/commands/create.d.ts +9 -0
  16. package/dist/commands/create.js +119 -0
  17. package/dist/commands/create.js.map +1 -0
  18. package/dist/commands/dev.d.ts +12 -0
  19. package/dist/commands/dev.js +107 -0
  20. package/dist/commands/dev.js.map +1 -0
  21. package/dist/commands/lib/flags.d.ts +10 -0
  22. package/dist/commands/lib/flags.js +33 -0
  23. package/dist/commands/lib/flags.js.map +1 -0
  24. package/dist/commands/lib/identity.d.ts +38 -0
  25. package/dist/commands/lib/identity.js +120 -0
  26. package/dist/commands/lib/identity.js.map +1 -0
  27. package/dist/commands/lib/project.d.ts +34 -0
  28. package/dist/commands/lib/project.js +78 -0
  29. package/dist/commands/lib/project.js.map +1 -0
  30. package/dist/commands/lib/report.d.ts +15 -0
  31. package/dist/commands/lib/report.js +75 -0
  32. package/dist/commands/lib/report.js.map +1 -0
  33. package/dist/commands/lib/starter.d.ts +41 -0
  34. package/dist/commands/lib/starter.js +136 -0
  35. package/dist/commands/lib/starter.js.map +1 -0
  36. package/dist/commands/lib/static-server.d.ts +45 -0
  37. package/dist/commands/lib/static-server.js +176 -0
  38. package/dist/commands/lib/static-server.js.map +1 -0
  39. package/dist/commands/pack.d.ts +5 -0
  40. package/dist/commands/pack.js +56 -0
  41. package/dist/commands/pack.js.map +1 -0
  42. package/dist/commands/registry.d.ts +7 -0
  43. package/dist/commands/registry.js +16 -0
  44. package/dist/commands/registry.js.map +1 -0
  45. package/dist/commands/run.d.ts +14 -0
  46. package/dist/commands/run.js +79 -0
  47. package/dist/commands/run.js.map +1 -0
  48. package/dist/commands/types.d.ts +34 -0
  49. package/dist/commands/types.js +10 -0
  50. package/dist/commands/types.js.map +1 -0
  51. package/dist/commands/validate.d.ts +6 -0
  52. package/dist/commands/validate.js +33 -0
  53. package/dist/commands/validate.js.map +1 -0
  54. package/dist/dev/assemble.d.ts +47 -0
  55. package/dist/dev/assemble.js +339 -0
  56. package/dist/dev/assemble.js.map +1 -0
  57. package/dist/dev/compile.d.ts +54 -0
  58. package/dist/dev/compile.js +215 -0
  59. package/dist/dev/compile.js.map +1 -0
  60. package/dist/dev/csp.d.ts +67 -0
  61. package/dist/dev/csp.js +260 -0
  62. package/dist/dev/csp.js.map +1 -0
  63. package/dist/dev/diagnostics.d.ts +21 -0
  64. package/dist/dev/diagnostics.js +22 -0
  65. package/dist/dev/diagnostics.js.map +1 -0
  66. package/dist/dev/envelope.d.ts +92 -0
  67. package/dist/dev/envelope.js +154 -0
  68. package/dist/dev/envelope.js.map +1 -0
  69. package/dist/dev/media.d.ts +53 -0
  70. package/dist/dev/media.js +128 -0
  71. package/dist/dev/media.js.map +1 -0
  72. package/dist/dev/needs-host.d.ts +2 -0
  73. package/dist/dev/needs-host.js +32 -0
  74. package/dist/dev/needs-host.js.map +1 -0
  75. package/dist/dev/output.d.ts +9 -0
  76. package/dist/dev/output.js +32 -0
  77. package/dist/dev/output.js.map +1 -0
  78. package/dist/dev/overlay.d.ts +21 -0
  79. package/dist/dev/overlay.js +59 -0
  80. package/dist/dev/overlay.js.map +1 -0
  81. package/dist/dev/server.d.ts +27 -0
  82. package/dist/dev/server.js +268 -0
  83. package/dist/dev/server.js.map +1 -0
  84. package/dist/dev/shell.d.ts +94 -0
  85. package/dist/dev/shell.js +267 -0
  86. package/dist/dev/shell.js.map +1 -0
  87. package/dist/dev/sources.d.ts +45 -0
  88. package/dist/dev/sources.js +165 -0
  89. package/dist/dev/sources.js.map +1 -0
  90. package/dist/dev/theme.d.ts +51 -0
  91. package/dist/dev/theme.js +185 -0
  92. package/dist/dev/theme.js.map +1 -0
  93. package/dist/dev/types.d.ts +40 -0
  94. package/dist/dev/types.js +11 -0
  95. package/dist/dev/types.js.map +1 -0
  96. package/dist/dev/watcher.d.ts +29 -0
  97. package/dist/dev/watcher.js +150 -0
  98. package/dist/dev/watcher.js.map +1 -0
  99. package/dist/host/definitions.d.ts +61 -0
  100. package/dist/host/definitions.js +136 -0
  101. package/dist/host/definitions.js.map +1 -0
  102. package/dist/host/http.d.ts +39 -0
  103. package/dist/host/http.js +156 -0
  104. package/dist/host/http.js.map +1 -0
  105. package/dist/host/local-host.d.ts +59 -0
  106. package/dist/host/local-host.js +420 -0
  107. package/dist/host/local-host.js.map +1 -0
  108. package/dist/host/oidc.d.ts +46 -0
  109. package/dist/host/oidc.js +192 -0
  110. package/dist/host/oidc.js.map +1 -0
  111. package/dist/host/query.d.ts +35 -0
  112. package/dist/host/query.js +158 -0
  113. package/dist/host/query.js.map +1 -0
  114. package/dist/host/receipts.d.ts +25 -0
  115. package/dist/host/receipts.js +72 -0
  116. package/dist/host/receipts.js.map +1 -0
  117. package/dist/host/records.d.ts +73 -0
  118. package/dist/host/records.js +229 -0
  119. package/dist/host/records.js.map +1 -0
  120. package/dist/host/tokens.d.ts +28 -0
  121. package/dist/host/tokens.js +52 -0
  122. package/dist/host/tokens.js.map +1 -0
  123. package/dist/host/types.d.ts +32 -0
  124. package/dist/host/types.js +11 -0
  125. package/dist/host/types.js.map +1 -0
  126. package/dist/index.d.ts +18 -0
  127. package/dist/index.js +51 -0
  128. package/dist/index.js.map +1 -0
  129. package/package.json +66 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../../../src/commands/lib/project.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The project folder as the commands see it: which files belong to the pack,\n * where the local pack cache lives, and how a pack source (folder or archive)\n * is opened.\n *\n * A project is a pwpack folder plus things that are never packed: the `.pwf/`\n * cache (installed pack builds), the `.gitignore` next to it, and anything\n * else outside the pack content (`packContent`: `pwpack.json`, `LICENSE`,\n * `NOTICE`, `README.md`, `app/`, `atoms/`, `media/`).\n */\nimport { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join, resolve } from 'node:path'\nimport {\n DEFAULT_IGNORED_NAMES,\n isSafePackPath,\n readFolder,\n readPwpack,\n type PackFolder,\n} from '@pwfabric/pack'\n\n/** The project-local working directory (pack cache, build output of other commands). */\nexport const PWF_DIR = '.pwf'\n/** Installed pack builds, one folder per `<id>@<version>`. */\nexport const PACK_CACHE_DIR = `${PWF_DIR}/packs`\n/** The note `create` writes into `atoms/`. Pack content (documentation, `DOC_FILE`), so it travels with the pack. */\nexport const ATOMS_README = 'atoms/README.md'\n\n/** Names skipped at any depth when a project is read: the defaults plus the cache and VCS ignore file. */\nexport const PROJECT_IGNORED_NAMES: ReadonlySet<string> = new Set([...DEFAULT_IGNORED_NAMES, PWF_DIR, '.gitignore'])\n\n/**\n * Reads a project folder into memory, without the cache. The author's tooling\n * (`package.json`, `scripts/`, …) is still in it — validation reports on it;\n * `packContent` is what is packed, installed and hashed.\n */\nexport async function readProject(dir: string): Promise<PackFolder> {\n const read = await readFolder(dir, { ignore: PROJECT_IGNORED_NAMES })\n // A dot entry at the root (`.editorconfig`, `.vscode/`, …) is never pack\n // content; it is the author's environment and is not reported on.\n return { files: new Map([...read.files].filter(([path]) => !path.startsWith('.'))) }\n}\n\n/**\n * Opens a pack source: a folder (read like a project, so its own `.pwf/`\n * cache never travels) or a `.pwpack` / `.tgz` archive.\n */\nexport async function readPackSource(path: string): Promise<PackFolder> {\n const st = await stat(path).catch(() => null)\n if (!st) throw new Error(`no such file or folder: ${path}`)\n if (st.isDirectory()) return readProject(path)\n return readPwpack(new Uint8Array(await readFile(path)))\n}\n\n/**\n * Relative folder of one pack build in the cache: `.pwf/packs/<id>@<version>`.\n * A scoped id (`@pwpack/acme/cards`) nests the way npm scopes do. Throws when\n * the id or version could escape the cache.\n */\nexport function cacheRelDir(id: string, version: string): string {\n const rel = `${id}@${version}`\n if (!isSafePackPath(rel) || /[<>:\"|?*]/.test(rel)) throw new Error(`pack id \"${id}\" with version \"${version}\" is not usable as a cache folder name`)\n return `${PACK_CACHE_DIR}/${rel}`\n}\n\n/** Absolute folder of one pack build in a project's cache. */\nexport function cacheDir(root: string, id: string, version: string): string {\n return join(root, ...cacheRelDir(id, version).split('/'))\n}\n\n/** Reads a cached pack build exactly as it was written: nothing is skipped. */\nexport async function readCachedPack(dir: string): Promise<PackFolder> {\n return readFolder(dir, { ignore: new Set() })\n}\n\n/** Writes a pack folder to disk, replacing whatever was there. */\nexport async function writePackFolder(dir: string, pack: PackFolder): Promise<void> {\n await rm(dir, { recursive: true, force: true })\n await mkdir(dir, { recursive: true })\n for (const [path, bytes] of pack.files) {\n const target = join(dir, ...path.split('/'))\n await mkdir(dirname(target), { recursive: true })\n await writeFile(target, bytes)\n }\n}\n\n/** Resolves a user-given path against the command's working directory. */\nexport function resolveFrom(cwd: string, path: string | undefined): string {\n return resolve(cwd, path ?? '.')\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { type PackFolder, type PackIssue, type PackValidation } from '@pwfabric/pack';
2
+ import type { CommandIO } from '../types.js';
3
+ /**
4
+ * Checks every `packs[]` pin against `.pwf/packs/<id>@<version>/`.
5
+ * A build that is not cached is a warning (`PIN_NOT_CACHED`: `pwf add` it);
6
+ * a cached build whose content hash differs from the pin is an error
7
+ * (`PIN_HASH_MISMATCH`).
8
+ */
9
+ export declare function checkPins(root: string, identity: PackValidation['identity']): Promise<PackIssue[]>;
10
+ /** `validatePack` plus {@link checkPins}, as one result. */
11
+ export declare function validateProject(root: string, pack: PackFolder): Promise<PackValidation>;
12
+ /** Prints issues grouped by severity, errors first. Prints nothing for an empty list. */
13
+ export declare function printIssues(io: CommandIO, issues: readonly PackIssue[]): void;
14
+ /** `n errors, n warnings` for a summary line. */
15
+ export declare function countLine(issues: readonly PackIssue[]): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Validation as the commands report it: the pack library's findings plus
3
+ * the project's pins checked against its local cache.
4
+ */
5
+ import { existsSync } from 'node:fs';
6
+ import { hashPack, validatePack } from '@pwfabric/pack';
7
+ import { cacheDir, cacheRelDir, readCachedPack } from './project.js';
8
+ import { pinsOf } from './identity.js';
9
+ /**
10
+ * Checks every `packs[]` pin against `.pwf/packs/<id>@<version>/`.
11
+ * A build that is not cached is a warning (`PIN_NOT_CACHED`: `pwf add` it);
12
+ * a cached build whose content hash differs from the pin is an error
13
+ * (`PIN_HASH_MISMATCH`).
14
+ */
15
+ export async function checkPins(root, identity) {
16
+ const issues = [];
17
+ for (const pin of pinsOf(identity)) {
18
+ let dir;
19
+ try {
20
+ dir = cacheDir(root, pin.id, pin.version);
21
+ }
22
+ catch (e) {
23
+ issues.push({ severity: 'error', code: 'PIN_INVALID', path: 'pwpack.json', message: e.message });
24
+ continue;
25
+ }
26
+ const rel = cacheRelDir(pin.id, pin.version);
27
+ if (!existsSync(dir)) {
28
+ issues.push({
29
+ severity: 'warning',
30
+ code: 'PIN_NOT_CACHED',
31
+ path: rel,
32
+ message: `${pin.id}@${pin.version} is pinned but not in ${rel}; run \`pwf add <its .pwpack or folder>\``,
33
+ });
34
+ continue;
35
+ }
36
+ const actual = hashPack(await readCachedPack(dir));
37
+ if (actual !== pin.hash) {
38
+ issues.push({
39
+ severity: 'error',
40
+ code: 'PIN_HASH_MISMATCH',
41
+ path: rel,
42
+ message: `${pin.id}@${pin.version} is pinned at ${pin.hash} but the cached build hashes ${actual}`,
43
+ });
44
+ }
45
+ }
46
+ return issues;
47
+ }
48
+ /** `validatePack` plus {@link checkPins}, as one result. */
49
+ export async function validateProject(root, pack) {
50
+ const result = validatePack(pack);
51
+ const pinIssues = await checkPins(root, result.identity);
52
+ const issues = [...result.issues, ...pinIssues];
53
+ return { ...result, issues, ok: !issues.some((i) => i.severity === 'error') };
54
+ }
55
+ const SEVERITIES = ['error', 'warning', 'info'];
56
+ const HEADINGS = { error: 'Errors', warning: 'Warnings', info: 'Notes' };
57
+ /** Prints issues grouped by severity, errors first. Prints nothing for an empty list. */
58
+ export function printIssues(io, issues) {
59
+ for (const severity of SEVERITIES) {
60
+ const group = issues.filter((i) => i.severity === severity);
61
+ if (group.length === 0)
62
+ continue;
63
+ const write = severity === 'error' ? io.err.bind(io) : io.out.bind(io);
64
+ write(`${HEADINGS[severity]} (${group.length}):`);
65
+ for (const issue of group)
66
+ write(` ${issue.code}${issue.path ? ` ${issue.path}` : ''}: ${issue.message}`);
67
+ }
68
+ }
69
+ /** `n errors, n warnings` for a summary line. */
70
+ export function countLine(issues) {
71
+ const n = (s) => issues.filter((i) => i.severity === s).length;
72
+ const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
73
+ return `${plural(n('error'), 'error')}, ${plural(n('warning'), 'warning')}`;
74
+ }
75
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../../src/commands/lib/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validation as the commands report it: the pack library's findings plus\n * the project's pins checked against its local cache.\n */\nimport { existsSync } from 'node:fs'\nimport { hashPack, validatePack, type PackFolder, type PackIssue, type PackValidation } from '@pwfabric/pack'\nimport type { CommandIO } from '../types.js'\nimport { cacheDir, cacheRelDir, readCachedPack } from './project.js'\nimport { pinsOf } from './identity.js'\n\n/**\n * Checks every `packs[]` pin against `.pwf/packs/<id>@<version>/`.\n * A build that is not cached is a warning (`PIN_NOT_CACHED`: `pwf add` it);\n * a cached build whose content hash differs from the pin is an error\n * (`PIN_HASH_MISMATCH`).\n */\nexport async function checkPins(root: string, identity: PackValidation['identity']): Promise<PackIssue[]> {\n const issues: PackIssue[] = []\n for (const pin of pinsOf(identity)) {\n let dir: string\n try {\n dir = cacheDir(root, pin.id, pin.version)\n } catch (e) {\n issues.push({ severity: 'error', code: 'PIN_INVALID', path: 'pwpack.json', message: (e as Error).message })\n continue\n }\n const rel = cacheRelDir(pin.id, pin.version)\n if (!existsSync(dir)) {\n issues.push({\n severity: 'warning',\n code: 'PIN_NOT_CACHED',\n path: rel,\n message: `${pin.id}@${pin.version} is pinned but not in ${rel}; run \\`pwf add <its .pwpack or folder>\\``,\n })\n continue\n }\n const actual = hashPack(await readCachedPack(dir))\n if (actual !== pin.hash) {\n issues.push({\n severity: 'error',\n code: 'PIN_HASH_MISMATCH',\n path: rel,\n message: `${pin.id}@${pin.version} is pinned at ${pin.hash} but the cached build hashes ${actual}`,\n })\n }\n }\n return issues\n}\n\n/** `validatePack` plus {@link checkPins}, as one result. */\nexport async function validateProject(root: string, pack: PackFolder): Promise<PackValidation> {\n const result = validatePack(pack)\n const pinIssues = await checkPins(root, result.identity)\n const issues = [...result.issues, ...pinIssues]\n return { ...result, issues, ok: !issues.some((i) => i.severity === 'error') }\n}\n\nconst SEVERITIES = ['error', 'warning', 'info'] as const\nconst HEADINGS = { error: 'Errors', warning: 'Warnings', info: 'Notes' } as const\n\n/** Prints issues grouped by severity, errors first. Prints nothing for an empty list. */\nexport function printIssues(io: CommandIO, issues: readonly PackIssue[]): void {\n for (const severity of SEVERITIES) {\n const group = issues.filter((i) => i.severity === severity)\n if (group.length === 0) continue\n const write = severity === 'error' ? io.err.bind(io) : io.out.bind(io)\n write(`${HEADINGS[severity]} (${group.length}):`)\n for (const issue of group) write(` ${issue.code}${issue.path ? ` ${issue.path}` : ''}: ${issue.message}`)\n }\n}\n\n/** `n errors, n warnings` for a summary line. */\nexport function countLine(issues: readonly PackIssue[]): string {\n const n = (s: PackIssue['severity']): number => issues.filter((i) => i.severity === s).length\n const plural = (count: number, word: string): string => `${count} ${word}${count === 1 ? '' : 's'}`\n return `${plural(n('error'), 'error')}, ${plural(n('warning'), 'warning')}`\n}\n"]}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What `pwf create` writes: the starter app's documents, composed only from
3
+ * the block types and themes the foundation pack actually carries.
4
+ */
5
+ import type { PackFolder, PackValidation } from '@pwfabric/pack';
6
+ export interface FoundationBlock {
7
+ /** Block type a receipt names (the manifest's `name`). */
8
+ readonly type: string;
9
+ /** Atom id, `<category>.<name>`. */
10
+ readonly id: string;
11
+ /** Prop names the manifest declares. */
12
+ readonly props: readonly string[];
13
+ }
14
+ export interface FoundationCatalog {
15
+ readonly blocks: readonly FoundationBlock[];
16
+ /** Theme ids (`ui-appearance` atoms). */
17
+ readonly themes: readonly string[];
18
+ }
19
+ /**
20
+ * Block types and themes of a pack, from its derived atoms. A block's type is
21
+ * the `name` of the `manifest.json` next to its component (falling back to the
22
+ * last segment of its id); its props are the manifest's `definition.props`.
23
+ */
24
+ export declare function catalogOf(pack: PackFolder, validation: PackValidation): FoundationCatalog;
25
+ export interface BlockNode {
26
+ readonly id: string;
27
+ readonly type: string;
28
+ readonly props: Record<string, unknown>;
29
+ }
30
+ /** Starter blocks for the home page, using only block types the catalog has. */
31
+ export declare function starterBlocks(catalog: FoundationCatalog, title: string): BlockNode[];
32
+ /** The theme the starter uses: `theme-zinc` when present (a neutral default), otherwise the first by id. */
33
+ export declare function starterTheme(catalog: FoundationCatalog): string | undefined;
34
+ /** `my-shop` → `My Shop`. */
35
+ export declare function titleOf(name: string): string;
36
+ export declare function starterPlan(name: string, title: string, blocks: readonly BlockNode[], foundationId: string, theme: string | undefined): unknown;
37
+ export declare function starterAppearance(theme: string | undefined): unknown;
38
+ export declare function starterNavigation(): unknown;
39
+ export declare function starterReceipt(blocks: readonly BlockNode[]): unknown;
40
+ export declare const ATOMS_README_TEXT = "# atoms/\n\nThis app's own reusable parts. Empty to start: the app is composed from the\nfoundation pack pinned in `pwpack.json`.\n\nAdd a block as `atoms/blocks/<category>/<name>/component.tsx` with a\n`manifest.json` next to it; see the pwpack format and block manifest\ndocuments of the PWFabric specification for every atom family.\n\nThis README is documentation, not an atom: it travels with the pack and\nvalidation notes it as such (DOC_FILE).\n";
41
+ export declare const GITIGNORE_TEXT = ".pwf/\n*.pwpack\n";
@@ -0,0 +1,136 @@
1
+ import { stripBom } from './identity.js';
2
+ function parseObject(bytes) {
3
+ if (!bytes)
4
+ return null;
5
+ try {
6
+ const v = JSON.parse(stripBom(new TextDecoder().decode(bytes)));
7
+ return v !== null && typeof v === 'object' && !Array.isArray(v) ? v : null;
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ /**
14
+ * Block types and themes of a pack, from its derived atoms. A block's type is
15
+ * the `name` of the `manifest.json` next to its component (falling back to the
16
+ * last segment of its id); its props are the manifest's `definition.props`.
17
+ */
18
+ export function catalogOf(pack, validation) {
19
+ const blocks = [];
20
+ const themes = [];
21
+ for (const atom of validation.atoms) {
22
+ if (atom.kind === 'ui-appearance') {
23
+ const manifest = parseObject(pack.files.get(atom.relPath));
24
+ themes.push(typeof manifest?.id === 'string' ? manifest.id : atom.id);
25
+ continue;
26
+ }
27
+ if (atom.kind !== 'block')
28
+ continue;
29
+ const dir = atom.relPath.slice(0, atom.relPath.lastIndexOf('/') + 1);
30
+ const manifest = parseObject(pack.files.get(`${dir}manifest.json`));
31
+ const name = typeof manifest?.name === 'string' ? manifest.name : atom.id.split('.').pop();
32
+ const definition = manifest?.definition;
33
+ const props = Array.isArray(definition?.props)
34
+ ? definition.props.flatMap((p) => p !== null && typeof p === 'object' && typeof p.name === 'string' ? [p.name] : [])
35
+ : [];
36
+ blocks.push({ type: name, id: atom.id, props });
37
+ }
38
+ return { blocks, themes };
39
+ }
40
+ /** The starter page, in order: a heading, a paragraph and a button. */
41
+ const SLOTS = [
42
+ { id: 'title', types: ['heading', 'title'], textProps: ['text', 'content', 'title', 'label'], text: (t) => t, extra: { level: 'h1' } },
43
+ {
44
+ id: 'intro',
45
+ types: ['text', 'paragraph', 'prose'],
46
+ textProps: ['content', 'text', 'body', 'markdown'],
47
+ text: () => 'Edit app/surfaces/home.receipt.json to change this page, then run pwf dev to see it.',
48
+ extra: {},
49
+ },
50
+ { id: 'cta', types: ['button'], textProps: ['label', 'text'], text: () => 'Get started', extra: { href: '/' } },
51
+ ];
52
+ /** Starter blocks for the home page, using only block types the catalog has. */
53
+ export function starterBlocks(catalog, title) {
54
+ const out = [];
55
+ for (const slot of SLOTS) {
56
+ const block = slot.types.map((t) => catalog.blocks.find((b) => b.type === t)).find((b) => b !== undefined);
57
+ if (!block)
58
+ continue;
59
+ const props = {};
60
+ const textProp = slot.textProps.find((p) => block.props.includes(p));
61
+ if (textProp)
62
+ props[textProp] = slot.text(title);
63
+ for (const [k, v] of Object.entries(slot.extra))
64
+ if (block.props.includes(k))
65
+ props[k] = v;
66
+ out.push({ id: slot.id, type: block.type, props });
67
+ }
68
+ return out;
69
+ }
70
+ /** The theme the starter uses: `theme-zinc` when present (a neutral default), otherwise the first by id. */
71
+ export function starterTheme(catalog) {
72
+ if (catalog.themes.includes('theme-zinc'))
73
+ return 'theme-zinc';
74
+ return [...catalog.themes].sort()[0];
75
+ }
76
+ /** `my-shop` → `My Shop`. */
77
+ export function titleOf(name) {
78
+ return name
79
+ .split(/[-_]+/)
80
+ .filter(Boolean)
81
+ .map((w) => w[0]?.toUpperCase() + w.slice(1))
82
+ .join(' ');
83
+ }
84
+ export function starterPlan(name, title, blocks, foundationId, theme) {
85
+ return {
86
+ title: title.slice(0, 80),
87
+ intent: {
88
+ purpose: `${title}: describe what this app is for`,
89
+ audience: 'Describe who will use it',
90
+ tone: 'Clear and friendly',
91
+ successCriteria: 'The home page renders',
92
+ },
93
+ theme: { direction: 'Clean and neutral', paletteHint: theme ?? 'the foundation default', status: 'applied' },
94
+ capabilities: [],
95
+ media: [],
96
+ customBlocks: [],
97
+ surfaces: [
98
+ {
99
+ key: 'home',
100
+ name: 'Home',
101
+ isEntry: true,
102
+ purpose: `The first page of ${name}`,
103
+ keyContent: blocks.map((b) => b.id),
104
+ linksTo: [],
105
+ usesCapabilities: [],
106
+ usesMedia: [],
107
+ usesCustomBlocks: [],
108
+ status: 'applied',
109
+ },
110
+ ],
111
+ packs: [{ id: foundationId }],
112
+ };
113
+ }
114
+ export function starterAppearance(theme) {
115
+ return theme ? { use: theme } : { config: { mode: { policy: 'light' } } };
116
+ }
117
+ export function starterNavigation() {
118
+ return { navbar: { links: [{ label: 'Home', href: '/' }] } };
119
+ }
120
+ export function starterReceipt(blocks) {
121
+ return { title: 'Home', blocks };
122
+ }
123
+ export const ATOMS_README_TEXT = `# atoms/
124
+
125
+ This app's own reusable parts. Empty to start: the app is composed from the
126
+ foundation pack pinned in \`pwpack.json\`.
127
+
128
+ Add a block as \`atoms/blocks/<category>/<name>/component.tsx\` with a
129
+ \`manifest.json\` next to it; see the pwpack format and block manifest
130
+ documents of the PWFabric specification for every atom family.
131
+
132
+ This README is documentation, not an atom: it travels with the pack and
133
+ validation notes it as such (DOC_FILE).
134
+ `;
135
+ export const GITIGNORE_TEXT = `.pwf/\n*.pwpack\n`;
136
+ //# sourceMappingURL=starter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"starter.js","sourceRoot":"","sources":["../../../src/commands/lib/starter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What `pwf create` writes: the starter app's documents, composed only from\n * the block types and themes the foundation pack actually carries.\n */\nimport type { PackFolder, PackValidation } from '@pwfabric/pack'\nimport { stripBom } from './identity.js'\n\nexport interface FoundationBlock {\n /** Block type a receipt names (the manifest's `name`). */\n readonly type: string\n /** Atom id, `<category>.<name>`. */\n readonly id: string\n /** Prop names the manifest declares. */\n readonly props: readonly string[]\n}\n\nexport interface FoundationCatalog {\n readonly blocks: readonly FoundationBlock[]\n /** Theme ids (`ui-appearance` atoms). */\n readonly themes: readonly string[]\n}\n\nfunction parseObject(bytes: Uint8Array | undefined): Record<string, unknown> | null {\n if (!bytes) return null\n try {\n const v: unknown = JSON.parse(stripBom(new TextDecoder().decode(bytes)))\n return v !== null && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null\n } catch {\n return null\n }\n}\n\n/**\n * Block types and themes of a pack, from its derived atoms. A block's type is\n * the `name` of the `manifest.json` next to its component (falling back to the\n * last segment of its id); its props are the manifest's `definition.props`.\n */\nexport function catalogOf(pack: PackFolder, validation: PackValidation): FoundationCatalog {\n const blocks: FoundationBlock[] = []\n const themes: string[] = []\n for (const atom of validation.atoms) {\n if (atom.kind === 'ui-appearance') {\n const manifest = parseObject(pack.files.get(atom.relPath))\n themes.push(typeof manifest?.id === 'string' ? manifest.id : atom.id)\n continue\n }\n if (atom.kind !== 'block') continue\n const dir = atom.relPath.slice(0, atom.relPath.lastIndexOf('/') + 1)\n const manifest = parseObject(pack.files.get(`${dir}manifest.json`))\n const name = typeof manifest?.name === 'string' ? manifest.name : (atom.id.split('.').pop() as string)\n const definition = manifest?.definition as { props?: unknown } | undefined\n const props = Array.isArray(definition?.props)\n ? (definition.props as unknown[]).flatMap((p) =>\n p !== null && typeof p === 'object' && typeof (p as { name?: unknown }).name === 'string' ? [(p as { name: string }).name] : []\n )\n : []\n blocks.push({ type: name, id: atom.id, props })\n }\n return { blocks, themes }\n}\n\ninterface Slot {\n readonly id: string\n readonly types: readonly string[]\n readonly textProps: readonly string[]\n readonly text: (title: string) => string\n readonly extra: Readonly<Record<string, unknown>>\n}\n\n/** The starter page, in order: a heading, a paragraph and a button. */\nconst SLOTS: readonly Slot[] = [\n { id: 'title', types: ['heading', 'title'], textProps: ['text', 'content', 'title', 'label'], text: (t) => t, extra: { level: 'h1' } },\n {\n id: 'intro',\n types: ['text', 'paragraph', 'prose'],\n textProps: ['content', 'text', 'body', 'markdown'],\n text: () => 'Edit app/surfaces/home.receipt.json to change this page, then run pwf dev to see it.',\n extra: {},\n },\n { id: 'cta', types: ['button'], textProps: ['label', 'text'], text: () => 'Get started', extra: { href: '/' } },\n]\n\nexport interface BlockNode {\n readonly id: string\n readonly type: string\n readonly props: Record<string, unknown>\n}\n\n/** Starter blocks for the home page, using only block types the catalog has. */\nexport function starterBlocks(catalog: FoundationCatalog, title: string): BlockNode[] {\n const out: BlockNode[] = []\n for (const slot of SLOTS) {\n const block = slot.types.map((t) => catalog.blocks.find((b) => b.type === t)).find((b) => b !== undefined)\n if (!block) continue\n const props: Record<string, unknown> = {}\n const textProp = slot.textProps.find((p) => block.props.includes(p))\n if (textProp) props[textProp] = slot.text(title)\n for (const [k, v] of Object.entries(slot.extra)) if (block.props.includes(k)) props[k] = v\n out.push({ id: slot.id, type: block.type, props })\n }\n return out\n}\n\n/** The theme the starter uses: `theme-zinc` when present (a neutral default), otherwise the first by id. */\nexport function starterTheme(catalog: FoundationCatalog): string | undefined {\n if (catalog.themes.includes('theme-zinc')) return 'theme-zinc'\n return [...catalog.themes].sort()[0]\n}\n\n/** `my-shop` → `My Shop`. */\nexport function titleOf(name: string): string {\n return name\n .split(/[-_]+/)\n .filter(Boolean)\n .map((w) => w[0]?.toUpperCase() + w.slice(1))\n .join(' ')\n}\n\nexport function starterPlan(name: string, title: string, blocks: readonly BlockNode[], foundationId: string, theme: string | undefined): unknown {\n return {\n title: title.slice(0, 80),\n intent: {\n purpose: `${title}: describe what this app is for`,\n audience: 'Describe who will use it',\n tone: 'Clear and friendly',\n successCriteria: 'The home page renders',\n },\n theme: { direction: 'Clean and neutral', paletteHint: theme ?? 'the foundation default', status: 'applied' },\n capabilities: [],\n media: [],\n customBlocks: [],\n surfaces: [\n {\n key: 'home',\n name: 'Home',\n isEntry: true,\n purpose: `The first page of ${name}`,\n keyContent: blocks.map((b) => b.id),\n linksTo: [],\n usesCapabilities: [],\n usesMedia: [],\n usesCustomBlocks: [],\n status: 'applied',\n },\n ],\n packs: [{ id: foundationId }],\n }\n}\n\nexport function starterAppearance(theme: string | undefined): unknown {\n return theme ? { use: theme } : { config: { mode: { policy: 'light' } } }\n}\n\nexport function starterNavigation(): unknown {\n return { navbar: { links: [{ label: 'Home', href: '/' }] } }\n}\n\nexport function starterReceipt(blocks: readonly BlockNode[]): unknown {\n return { title: 'Home', blocks }\n}\n\nexport const ATOMS_README_TEXT = `# atoms/\n\nThis app's own reusable parts. Empty to start: the app is composed from the\nfoundation pack pinned in \\`pwpack.json\\`.\n\nAdd a block as \\`atoms/blocks/<category>/<name>/component.tsx\\` with a\n\\`manifest.json\\` next to it; see the pwpack format and block manifest\ndocuments of the PWFabric specification for every atom family.\n\nThis README is documentation, not an atom: it travels with the pack and\nvalidation notes it as such (DOC_FILE).\n`\n\nexport const GITIGNORE_TEXT = `.pwf/\\n*.pwpack\\n`\n"]}
@@ -0,0 +1,45 @@
1
+ export declare const CONTENT_TYPES: Readonly<Record<string, string>>;
2
+ export declare function contentTypeOf(path: string): string;
3
+ /** Where the files come from: a folder on disk, or files held in memory (an opened archive). */
4
+ export type StaticSource = {
5
+ readonly dir: string;
6
+ } | {
7
+ readonly files: ReadonlyMap<string, Uint8Array>;
8
+ };
9
+ export interface StaticServerOptions {
10
+ readonly source: StaticSource;
11
+ /** 0 picks a free port. */
12
+ readonly port: number;
13
+ /** Default 127.0.0.1. */
14
+ readonly hostname?: string;
15
+ }
16
+ export interface StaticServer {
17
+ readonly url: string;
18
+ readonly port: number;
19
+ close(): Promise<void>;
20
+ }
21
+ /** The request path as a safe relative POSIX path, or null when it is malformed or escapes the root. */
22
+ export declare function requestPath(rawUrl: string): string | null;
23
+ /** Resolves a request to a file: the path itself, `<path>/index.html`, then the SPA fallback for page paths. */
24
+ export declare function resolveRequest(source: StaticSource, rel: string): Promise<{
25
+ path: string;
26
+ body: Uint8Array;
27
+ } | null>;
28
+ /** The static-host headers file a build writes beside `index.html`. */
29
+ export declare const HEADERS_FILE = "_headers";
30
+ /** One rule of a `_headers` file: a path pattern and the headers it adds. */
31
+ export interface HeaderRule {
32
+ readonly pattern: string;
33
+ readonly headers: readonly (readonly [string, string])[];
34
+ }
35
+ /**
36
+ * Parses the `_headers` convention of static hosts: a line that starts at
37
+ * column 0 is a path pattern (`/*`, `/assets/*`, `/about`), the indented
38
+ * `Name: value` lines under it are the headers it adds. `#` lines are
39
+ * comments.
40
+ */
41
+ export declare function parseHeadersFile(text: string): HeaderRule[];
42
+ /** Whether a `_headers` pattern matches a request path (`/` + the relative path). */
43
+ export declare function headerPatternMatches(pattern: string, path: string): boolean;
44
+ /** Starts the server; resolves once it is listening. */
45
+ export declare function startStaticServer(options: StaticServerOptions): Promise<StaticServer>;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * A small static file server for a built pwapp: correct content types, no
3
+ * directory listings, no path escapes, and an SPA fallback to `index.html`
4
+ * for page paths.
5
+ */
6
+ import { createServer } from 'node:http';
7
+ import { readFile, stat } from 'node:fs/promises';
8
+ import { extname, join, resolve, sep } from 'node:path';
9
+ import { withBaseHref } from '../../dev/shell.js';
10
+ export const CONTENT_TYPES = {
11
+ '.html': 'text/html; charset=utf-8',
12
+ '.js': 'text/javascript; charset=utf-8',
13
+ '.mjs': 'text/javascript; charset=utf-8',
14
+ '.json': 'application/json; charset=utf-8',
15
+ '.css': 'text/css; charset=utf-8',
16
+ '.svg': 'image/svg+xml',
17
+ '.map': 'application/json; charset=utf-8',
18
+ '.txt': 'text/plain; charset=utf-8',
19
+ '.png': 'image/png',
20
+ '.jpg': 'image/jpeg',
21
+ '.jpeg': 'image/jpeg',
22
+ '.gif': 'image/gif',
23
+ '.webp': 'image/webp',
24
+ '.avif': 'image/avif',
25
+ '.ico': 'image/x-icon',
26
+ '.woff': 'font/woff',
27
+ '.woff2': 'font/woff2',
28
+ '.wasm': 'application/wasm',
29
+ };
30
+ export function contentTypeOf(path) {
31
+ return CONTENT_TYPES[extname(path).toLowerCase()] ?? 'application/octet-stream';
32
+ }
33
+ /** The request path as a safe relative POSIX path, or null when it is malformed or escapes the root. */
34
+ export function requestPath(rawUrl) {
35
+ let pathname;
36
+ try {
37
+ pathname = decodeURIComponent(new URL(rawUrl, 'http://localhost').pathname);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ if (pathname.includes('\0') || pathname.includes('\\'))
43
+ return null;
44
+ const parts = [];
45
+ for (const seg of pathname.split('/')) {
46
+ if (seg === '' || seg === '.')
47
+ continue;
48
+ if (seg === '..')
49
+ return null;
50
+ parts.push(seg);
51
+ }
52
+ return parts.join('/');
53
+ }
54
+ async function load(source, rel) {
55
+ if ('files' in source)
56
+ return source.files.get(rel) ?? null;
57
+ const root = resolve(source.dir);
58
+ const abs = resolve(join(root, ...rel.split('/')));
59
+ if (abs !== root && !abs.startsWith(root + sep))
60
+ return null;
61
+ const st = await stat(abs).catch(() => null);
62
+ if (!st?.isFile())
63
+ return null;
64
+ return new Uint8Array(await readFile(abs));
65
+ }
66
+ /** Resolves a request to a file: the path itself, `<path>/index.html`, then the SPA fallback for page paths. */
67
+ export async function resolveRequest(source, rel) {
68
+ const candidates = rel === '' ? ['index.html'] : [rel, `${rel}/index.html`];
69
+ // A path whose last segment has no extension is a page path: answer it with the shell.
70
+ if (rel !== '' && extname(rel) === '')
71
+ candidates.push('index.html');
72
+ for (const path of candidates) {
73
+ const body = await load(source, path);
74
+ if (body)
75
+ return { path, body };
76
+ }
77
+ return null;
78
+ }
79
+ const TEXT = { 'content-type': 'text/plain; charset=utf-8' };
80
+ /** The static-host headers file a build writes beside `index.html`. */
81
+ export const HEADERS_FILE = '_headers';
82
+ /**
83
+ * Parses the `_headers` convention of static hosts: a line that starts at
84
+ * column 0 is a path pattern (`/*`, `/assets/*`, `/about`), the indented
85
+ * `Name: value` lines under it are the headers it adds. `#` lines are
86
+ * comments.
87
+ */
88
+ export function parseHeadersFile(text) {
89
+ const rules = [];
90
+ for (const raw of text.split(/\r?\n/)) {
91
+ if (!raw.trim() || raw.trim().startsWith('#'))
92
+ continue;
93
+ if (!/^\s/.test(raw)) {
94
+ rules.push({ pattern: raw.trim(), headers: [] });
95
+ continue;
96
+ }
97
+ const at = raw.indexOf(':');
98
+ const rule = rules[rules.length - 1];
99
+ if (at > 0 && rule)
100
+ rule.headers.push([raw.slice(0, at).trim(), raw.slice(at + 1).trim()]);
101
+ }
102
+ return rules;
103
+ }
104
+ /** Whether a `_headers` pattern matches a request path (`/` + the relative path). */
105
+ export function headerPatternMatches(pattern, path) {
106
+ if (pattern.endsWith('*'))
107
+ return path.startsWith(pattern.slice(0, -1));
108
+ return pattern === path;
109
+ }
110
+ /** The headers the `_headers` file adds for a relative path. */
111
+ async function extraHeaders(source, rel) {
112
+ const out = {};
113
+ const file = await load(source, HEADERS_FILE);
114
+ if (!file)
115
+ return out;
116
+ for (const rule of parseHeadersFile(new TextDecoder().decode(file))) {
117
+ if (!headerPatternMatches(rule.pattern, `/${rel}`))
118
+ continue;
119
+ for (const [name, value] of rule.headers)
120
+ out[name.toLowerCase()] = value;
121
+ }
122
+ return out;
123
+ }
124
+ async function handle(source, req, res) {
125
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
126
+ res.writeHead(405, { ...TEXT, allow: 'GET, HEAD' }).end('method not allowed\n');
127
+ return;
128
+ }
129
+ const rel = requestPath(req.url ?? '/');
130
+ // The headers file configures the server; it is not part of the site.
131
+ const found = rel === null || rel === HEADERS_FILE ? null : await resolveRequest(source, rel);
132
+ if (!found) {
133
+ res.writeHead(404, TEXT).end('not found\n');
134
+ return;
135
+ }
136
+ // A page path answered with the shell gets the app's base (see withBaseHref).
137
+ const body = found.path === 'index.html' && rel !== '' && rel !== 'index.html' ? withBaseHref(found.body) : found.body;
138
+ res.writeHead(200, {
139
+ ...(await extraHeaders(source, rel ?? '')),
140
+ 'content-type': contentTypeOf(found.path),
141
+ 'content-length': String(body.byteLength),
142
+ 'cache-control': 'no-cache',
143
+ });
144
+ if (req.method === 'HEAD')
145
+ res.end();
146
+ else
147
+ res.end(body);
148
+ }
149
+ /** Starts the server; resolves once it is listening. */
150
+ export function startStaticServer(options) {
151
+ const hostname = options.hostname ?? '127.0.0.1';
152
+ const server = createServer((req, res) => {
153
+ handle(options.source, req, res).catch(() => {
154
+ if (!res.headersSent)
155
+ res.writeHead(500, TEXT);
156
+ res.end('internal error\n');
157
+ });
158
+ });
159
+ return new Promise((resolveStart, reject) => {
160
+ server.once('error', reject);
161
+ server.listen(options.port, hostname, () => {
162
+ server.off('error', reject);
163
+ const { port } = server.address();
164
+ const host = hostname.includes(':') ? `[${hostname}]` : hostname;
165
+ resolveStart({
166
+ url: `http://${host}:${port}/`,
167
+ port,
168
+ close: () => new Promise((done, fail) => {
169
+ server.closeAllConnections();
170
+ server.close((err) => (err ? fail(err) : done()));
171
+ }),
172
+ });
173
+ });
174
+ });
175
+ }
176
+ //# sourceMappingURL=static-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static-server.js","sourceRoot":"","sources":["../../../src/commands/lib/static-server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A small static file server for a built pwapp: correct content types, no\n * directory listings, no path escapes, and an SPA fallback to `index.html`\n * for page paths.\n */\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { readFile, stat } from 'node:fs/promises'\nimport { extname, join, resolve, sep } from 'node:path'\nimport type { AddressInfo } from 'node:net'\nimport { withBaseHref } from '../../dev/shell.js'\n\nexport const CONTENT_TYPES: Readonly<Record<string, string>> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.map': 'application/json; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.wasm': 'application/wasm',\n}\n\nexport function contentTypeOf(path: string): string {\n return CONTENT_TYPES[extname(path).toLowerCase()] ?? 'application/octet-stream'\n}\n\n/** Where the files come from: a folder on disk, or files held in memory (an opened archive). */\nexport type StaticSource = { readonly dir: string } | { readonly files: ReadonlyMap<string, Uint8Array> }\n\nexport interface StaticServerOptions {\n readonly source: StaticSource\n /** 0 picks a free port. */\n readonly port: number\n /** Default 127.0.0.1. */\n readonly hostname?: string\n}\n\nexport interface StaticServer {\n readonly url: string\n readonly port: number\n close(): Promise<void>\n}\n\n/** The request path as a safe relative POSIX path, or null when it is malformed or escapes the root. */\nexport function requestPath(rawUrl: string): string | null {\n let pathname: string\n try {\n pathname = decodeURIComponent(new URL(rawUrl, 'http://localhost').pathname)\n } catch {\n return null\n }\n if (pathname.includes('\\0') || pathname.includes('\\\\')) return null\n const parts: string[] = []\n for (const seg of pathname.split('/')) {\n if (seg === '' || seg === '.') continue\n if (seg === '..') return null\n parts.push(seg)\n }\n return parts.join('/')\n}\n\nasync function load(source: StaticSource, rel: string): Promise<Uint8Array | null> {\n if ('files' in source) return source.files.get(rel) ?? null\n const root = resolve(source.dir)\n const abs = resolve(join(root, ...rel.split('/')))\n if (abs !== root && !abs.startsWith(root + sep)) return null\n const st = await stat(abs).catch(() => null)\n if (!st?.isFile()) return null\n return new Uint8Array(await readFile(abs))\n}\n\n/** Resolves a request to a file: the path itself, `<path>/index.html`, then the SPA fallback for page paths. */\nexport async function resolveRequest(source: StaticSource, rel: string): Promise<{ path: string; body: Uint8Array } | null> {\n const candidates = rel === '' ? ['index.html'] : [rel, `${rel}/index.html`]\n // A path whose last segment has no extension is a page path: answer it with the shell.\n if (rel !== '' && extname(rel) === '') candidates.push('index.html')\n for (const path of candidates) {\n const body = await load(source, path)\n if (body) return { path, body }\n }\n return null\n}\n\nconst TEXT = { 'content-type': 'text/plain; charset=utf-8' }\n\n/** The static-host headers file a build writes beside `index.html`. */\nexport const HEADERS_FILE = '_headers'\n\n/** One rule of a `_headers` file: a path pattern and the headers it adds. */\nexport interface HeaderRule {\n readonly pattern: string\n readonly headers: readonly (readonly [string, string])[]\n}\n\n/**\n * Parses the `_headers` convention of static hosts: a line that starts at\n * column 0 is a path pattern (`/*`, `/assets/*`, `/about`), the indented\n * `Name: value` lines under it are the headers it adds. `#` lines are\n * comments.\n */\nexport function parseHeadersFile(text: string): HeaderRule[] {\n const rules: { pattern: string; headers: [string, string][] }[] = []\n for (const raw of text.split(/\\r?\\n/)) {\n if (!raw.trim() || raw.trim().startsWith('#')) continue\n if (!/^\\s/.test(raw)) {\n rules.push({ pattern: raw.trim(), headers: [] })\n continue\n }\n const at = raw.indexOf(':')\n const rule = rules[rules.length - 1]\n if (at > 0 && rule) rule.headers.push([raw.slice(0, at).trim(), raw.slice(at + 1).trim()])\n }\n return rules\n}\n\n/** Whether a `_headers` pattern matches a request path (`/` + the relative path). */\nexport function headerPatternMatches(pattern: string, path: string): boolean {\n if (pattern.endsWith('*')) return path.startsWith(pattern.slice(0, -1))\n return pattern === path\n}\n\n/** The headers the `_headers` file adds for a relative path. */\nasync function extraHeaders(source: StaticSource, rel: string): Promise<Record<string, string>> {\n const out: Record<string, string> = {}\n const file = await load(source, HEADERS_FILE)\n if (!file) return out\n for (const rule of parseHeadersFile(new TextDecoder().decode(file))) {\n if (!headerPatternMatches(rule.pattern, `/${rel}`)) continue\n for (const [name, value] of rule.headers) out[name.toLowerCase()] = value\n }\n return out\n}\n\nasync function handle(source: StaticSource, req: IncomingMessage, res: ServerResponse): Promise<void> {\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n res.writeHead(405, { ...TEXT, allow: 'GET, HEAD' }).end('method not allowed\\n')\n return\n }\n const rel = requestPath(req.url ?? '/')\n // The headers file configures the server; it is not part of the site.\n const found = rel === null || rel === HEADERS_FILE ? null : await resolveRequest(source, rel)\n if (!found) {\n res.writeHead(404, TEXT).end('not found\\n')\n return\n }\n // A page path answered with the shell gets the app's base (see withBaseHref).\n const body = found.path === 'index.html' && rel !== '' && rel !== 'index.html' ? withBaseHref(found.body) : found.body\n res.writeHead(200, {\n ...(await extraHeaders(source, rel ?? '')),\n 'content-type': contentTypeOf(found.path),\n 'content-length': String(body.byteLength),\n 'cache-control': 'no-cache',\n })\n if (req.method === 'HEAD') res.end()\n else res.end(body)\n}\n\n/** Starts the server; resolves once it is listening. */\nexport function startStaticServer(options: StaticServerOptions): Promise<StaticServer> {\n const hostname = options.hostname ?? '127.0.0.1'\n const server = createServer((req, res) => {\n handle(options.source, req, res).catch(() => {\n if (!res.headersSent) res.writeHead(500, TEXT)\n res.end('internal error\\n')\n })\n })\n return new Promise((resolveStart, reject) => {\n server.once('error', reject)\n server.listen(options.port, hostname, () => {\n server.off('error', reject)\n const { port } = server.address() as AddressInfo\n const host = hostname.includes(':') ? `[${hostname}]` : hostname\n resolveStart({\n url: `http://${host}:${port}/`,\n port,\n close: () =>\n new Promise<void>((done, fail) => {\n server.closeAllConnections()\n server.close((err) => (err ? fail(err) : done()))\n }),\n })\n })\n })\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { Command, CommandIO, ParsedArgs } from './types.js';
2
+ /** `<name>-<version>.pwpack`, where name is the last segment of the pack id. */
3
+ export declare function archiveName(id: string, version: string): string;
4
+ export declare function runPack(args: ParsedArgs, io: CommandIO): Promise<number>;
5
+ export declare const pack: Command;