@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,185 @@
1
+ /**
2
+ * The app's appearance: resolving a theme named by `use` against the themes
3
+ * the app and its packs carry, and the stylesheet the shell links so the page
4
+ * has its identity before any script runs.
5
+ */
6
+ import { DEFAULT_APPEARANCE_THEME, findAllowedWebfont, mergeSurfaceThemes, parseFontStack, resolveAppearanceMode, resolveDarkTheme, surfaceThemeToCSS, } from '@pwfabric/contracts';
7
+ import { isRecord } from './sources.js';
8
+ /** Builds a {@link ThemeAtom} from a `ui-appearance` manifest, or null when it carries no token set. */
9
+ export function themeAtomOf(id, manifest, from) {
10
+ if (!isRecord(manifest) || !isRecord(manifest['theme']))
11
+ return null;
12
+ const manifestId = typeof manifest['id'] === 'string' ? manifest['id'] : undefined;
13
+ return {
14
+ id,
15
+ ...(manifestId && manifestId !== id ? { manifestId } : {}),
16
+ theme: manifest['theme'],
17
+ ...(isRecord(manifest['darkTheme']) ? { darkTheme: manifest['darkTheme'] } : {}),
18
+ from,
19
+ };
20
+ }
21
+ /**
22
+ * The theme a name points at. Matched as given, then as `theme-<name>`, then
23
+ * with an `appearance-` / `theme-` prefix removed; the first atom wins (the
24
+ * app's own themes come before its packs').
25
+ */
26
+ export function findTheme(name, themes) {
27
+ const bare = name.trim().replace(/^(appearance|theme)-/, '');
28
+ const wanted = [name.trim(), `theme-${bare}`, bare];
29
+ for (const w of wanted) {
30
+ const hit = themes.find((t) => t.id === w || t.manifestId === w);
31
+ if (hit)
32
+ return hit;
33
+ }
34
+ return undefined;
35
+ }
36
+ /** Per-group merge, `over` winning. */
37
+ export function mergeGroups(base, over) {
38
+ const out = {};
39
+ for (const [k, v] of Object.entries(base))
40
+ out[k] = isRecord(v) ? { ...v } : v;
41
+ if (isRecord(over)) {
42
+ for (const [k, v] of Object.entries(over)) {
43
+ const prev = out[k];
44
+ out[k] = isRecord(v) && isRecord(prev) ? { ...prev, ...v } : v;
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+ function resolveConfig(doc, themes, diagnostics, path) {
50
+ const use = doc['use'];
51
+ if (typeof use !== 'string' || !use.trim())
52
+ return doc;
53
+ const theme = findTheme(use, themes);
54
+ if (!theme) {
55
+ diagnostics.push({
56
+ severity: 'warning',
57
+ code: 'THEME_NOT_FOUND',
58
+ message: `appearance names theme "${use}", which neither the app nor its packs carry (known: ${themes.map((t) => t.id).join(', ') || 'none'}); the runner falls back to its built-in presets`,
59
+ path,
60
+ });
61
+ return doc;
62
+ }
63
+ const { use: _use, ...rest } = doc;
64
+ const out = { ...rest, theme: mergeGroups(theme.theme, rest['theme']) };
65
+ if (theme.darkTheme)
66
+ out['darkTheme'] = mergeGroups(theme.darkTheme, rest['darkTheme']);
67
+ return out;
68
+ }
69
+ /**
70
+ * The app-wide appearance as the envelope carries it. A theme named by `use`
71
+ * is resolved to its tokens here, so the published app does not depend on
72
+ * the runner knowing the name. The three shapes a reader accepts are kept:
73
+ * a reference (`{ config }`) stays a reference, everything else becomes a
74
+ * bare configuration.
75
+ */
76
+ export function resolveAppearance(doc, themes, diagnostics, path = 'app/definitions/appearance.json') {
77
+ if (!isRecord(doc))
78
+ return undefined;
79
+ if (isRecord(doc['config']))
80
+ return { ...doc, config: resolveConfig(doc['config'], themes, diagnostics, path) };
81
+ return resolveConfig(doc, themes, diagnostics, path);
82
+ }
83
+ /** The configuration inside any of the three appearance shapes. */
84
+ export function appearanceConfigOf(doc) {
85
+ if (!doc)
86
+ return undefined;
87
+ return isRecord(doc['config']) ? doc['config'] : doc;
88
+ }
89
+ function declList(vars) {
90
+ return Object.entries(vars)
91
+ .map(([name, value]) => ` ${name}: ${value};`)
92
+ .join('\n');
93
+ }
94
+ function modeBlocks(policy, light, dark, darkOverLight) {
95
+ if (policy === 'light')
96
+ return `:root, [data-pw-surface] {\n${light}\n}`;
97
+ if (policy === 'dark')
98
+ return `:root, [data-pw-surface] {\n${darkOverLight}\n}`;
99
+ return `:root, [data-pw-surface] {
100
+ ${light}
101
+ }
102
+
103
+ :root[data-pw-theme="dark"],
104
+ [data-pw-surface][data-pw-theme="dark"] {
105
+ ${dark}
106
+ }
107
+
108
+ @media (prefers-color-scheme: dark) {
109
+ :root:not([data-pw-theme="light"]),
110
+ [data-pw-surface]:not([data-pw-theme="light"]) {
111
+ ${dark}
112
+ }
113
+ }`;
114
+ }
115
+ /**
116
+ * `assets/theme.css`: the full token set as custom properties (light, the
117
+ * dark variant per the mode policy) and the document binding — page margin,
118
+ * body font and colours — so the app wears its identity before hydration.
119
+ */
120
+ export function buildThemeCss(config) {
121
+ const cfg = config;
122
+ const { policy, contrast } = resolveAppearanceMode(cfg);
123
+ const options = { contrast };
124
+ const lightVars = surfaceThemeToCSS(cfg?.theme, DEFAULT_APPEARANCE_THEME, undefined, options);
125
+ if (isRecord(cfg?.styleVariables)) {
126
+ for (const [key, value] of Object.entries(cfg.styleVariables)) {
127
+ if (typeof value !== 'string')
128
+ continue;
129
+ lightVars[key.startsWith('--') ? key : `--${key}`] = value;
130
+ }
131
+ }
132
+ const light = mergeSurfaceThemes(DEFAULT_APPEARANCE_THEME, cfg?.theme);
133
+ const darkVars = surfaceThemeToCSS(resolveDarkTheme(light, cfg?.darkTheme), undefined, undefined, options);
134
+ return `${modeBlocks(policy, declList(lightVars), declList(darkVars), declList({ ...lightVars, ...darkVars }))}
135
+
136
+ html, body {
137
+ margin: 0;
138
+ font-family: var(--pw-font-family, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);
139
+ font-size: var(--pw-font-size-base, 16px);
140
+ color: var(--pw-color-text, #1e293b);
141
+ background: var(--pw-color-background, #ffffff);
142
+ }
143
+ h1, h2, h3, h4, h5, h6 {
144
+ font-family: var(--pw-font-family-heading, inherit);
145
+ }
146
+ code, pre, kbd {
147
+ font-family: var(--pw-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
148
+ }
149
+ `;
150
+ }
151
+ /**
152
+ * The webfonts a theme names and may load: the first family of the body,
153
+ * heading and mono stacks, and only when it is on the engine's allow-list. A
154
+ * free-text family never becomes a URL.
155
+ */
156
+ export function themeWebfonts(config) {
157
+ const theme = config?.['theme'];
158
+ const typography = isRecord(theme) && isRecord(theme['typography']) ? theme['typography'] : undefined;
159
+ const out = [];
160
+ for (const key of ['fontFamily', 'headingFontFamily', 'monoFontFamily']) {
161
+ const stack = typography?.[key];
162
+ const first = parseFontStack(typeof stack === 'string' ? stack : undefined)[0];
163
+ const font = first === undefined ? undefined : findAllowedWebfont(first);
164
+ if (font && !out.some((f) => f.family === font.family))
165
+ out.push(font);
166
+ }
167
+ return out;
168
+ }
169
+ /** One stylesheet address for the families, or null when there are none. */
170
+ export function webfontStylesheetHref(fonts) {
171
+ if (fonts.length === 0)
172
+ return null;
173
+ const families = fonts.map((f) => {
174
+ const weights = [...new Set(f.weights)].sort((a, b) => a - b);
175
+ const name = encodeURIComponent(f.family).replace(/%20/g, '+');
176
+ if (weights.length === 0)
177
+ return `family=${name}`;
178
+ if (!f.italic)
179
+ return `family=${name}:wght@${weights.join(';')}`;
180
+ const tuples = [...weights.map((w) => `0,${w}`), ...weights.map((w) => `1,${w}`)];
181
+ return `family=${name}:ital,wght@${tuples.join(';')}`;
182
+ });
183
+ return `https://fonts.googleapis.com/css2?${families.join('&')}&display=swap`;
184
+ }
185
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/dev/theme.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The app's appearance: resolving a theme named by `use` against the themes\n * the app and its packs carry, and the stylesheet the shell links so the page\n * has its identity before any script runs.\n */\nimport {\n DEFAULT_APPEARANCE_THEME,\n findAllowedWebfont,\n mergeSurfaceThemes,\n parseFontStack,\n resolveAppearanceMode,\n resolveDarkTheme,\n surfaceThemeToCSS,\n type AllowedWebfont,\n type SurfaceTheme,\n} from '@pwfabric/contracts'\nimport type { AssembleDiagnostic } from './diagnostics.js'\nimport { isRecord } from './sources.js'\n\n/** A `ui-appearance` atom an app can name: its id and its token sets. */\nexport interface ThemeAtom {\n readonly id: string\n /** The manifest's own `id`, when it differs from the path-derived one. */\n readonly manifestId?: string\n readonly theme: Record<string, unknown>\n readonly darkTheme?: Record<string, unknown>\n /** Where it came from, for diagnostics. */\n readonly from: string\n}\n\n/** Builds a {@link ThemeAtom} from a `ui-appearance` manifest, or null when it carries no token set. */\nexport function themeAtomOf(id: string, manifest: unknown, from: string): ThemeAtom | null {\n if (!isRecord(manifest) || !isRecord(manifest['theme'])) return null\n const manifestId = typeof manifest['id'] === 'string' ? manifest['id'] : undefined\n return {\n id,\n ...(manifestId && manifestId !== id ? { manifestId } : {}),\n theme: manifest['theme'],\n ...(isRecord(manifest['darkTheme']) ? { darkTheme: manifest['darkTheme'] } : {}),\n from,\n }\n}\n\n/**\n * The theme a name points at. Matched as given, then as `theme-<name>`, then\n * with an `appearance-` / `theme-` prefix removed; the first atom wins (the\n * app's own themes come before its packs').\n */\nexport function findTheme(name: string, themes: readonly ThemeAtom[]): ThemeAtom | undefined {\n const bare = name.trim().replace(/^(appearance|theme)-/, '')\n const wanted = [name.trim(), `theme-${bare}`, bare]\n for (const w of wanted) {\n const hit = themes.find((t) => t.id === w || t.manifestId === w)\n if (hit) return hit\n }\n return undefined\n}\n\n/** Per-group merge, `over` winning. */\nexport function mergeGroups(base: Record<string, unknown>, over: unknown): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(base)) out[k] = isRecord(v) ? { ...v } : v\n if (isRecord(over)) {\n for (const [k, v] of Object.entries(over)) {\n const prev = out[k]\n out[k] = isRecord(v) && isRecord(prev) ? { ...prev, ...v } : v\n }\n }\n return out\n}\n\nfunction resolveConfig(\n doc: Record<string, unknown>,\n themes: readonly ThemeAtom[],\n diagnostics: AssembleDiagnostic[],\n path: string\n): Record<string, unknown> {\n const use = doc['use']\n if (typeof use !== 'string' || !use.trim()) return doc\n const theme = findTheme(use, themes)\n if (!theme) {\n diagnostics.push({\n severity: 'warning',\n code: 'THEME_NOT_FOUND',\n message: `appearance names theme \"${use}\", which neither the app nor its packs carry (known: ${\n themes.map((t) => t.id).join(', ') || 'none'\n }); the runner falls back to its built-in presets`,\n path,\n })\n return doc\n }\n const { use: _use, ...rest } = doc\n const out: Record<string, unknown> = { ...rest, theme: mergeGroups(theme.theme, rest['theme']) }\n if (theme.darkTheme) out['darkTheme'] = mergeGroups(theme.darkTheme, rest['darkTheme'])\n return out\n}\n\n/**\n * The app-wide appearance as the envelope carries it. A theme named by `use`\n * is resolved to its tokens here, so the published app does not depend on\n * the runner knowing the name. The three shapes a reader accepts are kept:\n * a reference (`{ config }`) stays a reference, everything else becomes a\n * bare configuration.\n */\nexport function resolveAppearance(\n doc: unknown,\n themes: readonly ThemeAtom[],\n diagnostics: AssembleDiagnostic[],\n path = 'app/definitions/appearance.json'\n): Record<string, unknown> | undefined {\n if (!isRecord(doc)) return undefined\n if (isRecord(doc['config'])) return { ...doc, config: resolveConfig(doc['config'], themes, diagnostics, path) }\n return resolveConfig(doc, themes, diagnostics, path)\n}\n\n/** The configuration inside any of the three appearance shapes. */\nexport function appearanceConfigOf(doc: Record<string, unknown> | undefined): Record<string, unknown> | undefined {\n if (!doc) return undefined\n return isRecord(doc['config']) ? doc['config'] : doc\n}\n\nfunction declList(vars: Record<string, string>): string {\n return Object.entries(vars)\n .map(([name, value]) => ` ${name}: ${value};`)\n .join('\\n')\n}\n\nfunction modeBlocks(policy: 'light' | 'dark' | 'system', light: string, dark: string, darkOverLight: string): string {\n if (policy === 'light') return `:root, [data-pw-surface] {\\n${light}\\n}`\n if (policy === 'dark') return `:root, [data-pw-surface] {\\n${darkOverLight}\\n}`\n return `:root, [data-pw-surface] {\n${light}\n}\n\n:root[data-pw-theme=\"dark\"],\n[data-pw-surface][data-pw-theme=\"dark\"] {\n${dark}\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not([data-pw-theme=\"light\"]),\n [data-pw-surface]:not([data-pw-theme=\"light\"]) {\n${dark}\n }\n}`\n}\n\n/**\n * `assets/theme.css`: the full token set as custom properties (light, the\n * dark variant per the mode policy) and the document binding — page margin,\n * body font and colours — so the app wears its identity before hydration.\n */\nexport function buildThemeCss(config: Record<string, unknown> | undefined): string {\n const cfg = config as { theme?: SurfaceTheme; darkTheme?: SurfaceTheme; styleVariables?: Record<string, unknown> } | undefined\n const { policy, contrast } = resolveAppearanceMode(cfg)\n const options = { contrast }\n const lightVars = surfaceThemeToCSS(cfg?.theme, DEFAULT_APPEARANCE_THEME, undefined, options)\n if (isRecord(cfg?.styleVariables)) {\n for (const [key, value] of Object.entries(cfg.styleVariables)) {\n if (typeof value !== 'string') continue\n lightVars[key.startsWith('--') ? key : `--${key}`] = value\n }\n }\n const light = mergeSurfaceThemes(DEFAULT_APPEARANCE_THEME, cfg?.theme)\n const darkVars = surfaceThemeToCSS(resolveDarkTheme(light, cfg?.darkTheme), undefined, undefined, options)\n return `${modeBlocks(policy, declList(lightVars), declList(darkVars), declList({ ...lightVars, ...darkVars }))}\n\nhtml, body {\n margin: 0;\n font-family: var(--pw-font-family, ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n font-size: var(--pw-font-size-base, 16px);\n color: var(--pw-color-text, #1e293b);\n background: var(--pw-color-background, #ffffff);\n}\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--pw-font-family-heading, inherit);\n}\ncode, pre, kbd {\n font-family: var(--pw-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace);\n}\n`\n}\n\n/**\n * The webfonts a theme names and may load: the first family of the body,\n * heading and mono stacks, and only when it is on the engine's allow-list. A\n * free-text family never becomes a URL.\n */\nexport function themeWebfonts(config: Record<string, unknown> | undefined): AllowedWebfont[] {\n const theme = config?.['theme']\n const typography = isRecord(theme) && isRecord(theme['typography']) ? theme['typography'] : undefined\n const out: AllowedWebfont[] = []\n for (const key of ['fontFamily', 'headingFontFamily', 'monoFontFamily']) {\n const stack = typography?.[key]\n const first = parseFontStack(typeof stack === 'string' ? stack : undefined)[0]\n const font = first === undefined ? undefined : findAllowedWebfont(first)\n if (font && !out.some((f) => f.family === font.family)) out.push(font)\n }\n return out\n}\n\n/** One stylesheet address for the families, or null when there are none. */\nexport function webfontStylesheetHref(fonts: readonly AllowedWebfont[]): string | null {\n if (fonts.length === 0) return null\n const families = fonts.map((f) => {\n const weights = [...new Set(f.weights)].sort((a, b) => a - b)\n const name = encodeURIComponent(f.family).replace(/%20/g, '+')\n if (weights.length === 0) return `family=${name}`\n if (!f.italic) return `family=${name}:wght@${weights.join(';')}`\n const tuples = [...weights.map((w) => `0,${w}`), ...weights.map((w) => `1,${w}`)]\n return `family=${name}:ital,wght@${tuples.join(';')}`\n })\n return `https://fonts.googleapis.com/css2?${families.join('&')}&display=swap`\n}\n"]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The contract between `pwf dev` (./server.ts, written by the dev lane) and
3
+ * the rest of the CLI.
4
+ *
5
+ * `pwf dev` watches a pwpack folder, compiles its atoms with the engine's
6
+ * compiler, builds an envelope from the folder, and serves a page that boots
7
+ * the runner against it. When the app declares data or sign-in, it mounts a
8
+ * local host (../host) that implements the session and data protocols.
9
+ */
10
+ import type { LocalHost } from '../host/types.js';
11
+ export interface DevServerOptions {
12
+ /** Absolute path of the pwpack folder to serve. */
13
+ readonly root: string;
14
+ /** Port to listen on; 0 picks a free port. Default 4600. */
15
+ readonly port?: number;
16
+ /** Host interface to bind. Default 127.0.0.1. */
17
+ readonly hostname?: string;
18
+ /** Mount a local host for data and sign-in. Default: only when the app declares them. */
19
+ readonly localHost?: boolean | LocalHost;
20
+ /** Called with human-readable progress lines. */
21
+ readonly log?: (line: string) => void;
22
+ }
23
+ export interface DevServer {
24
+ /** The URL the app is served at, e.g. http://127.0.0.1:4600/. */
25
+ readonly url: string;
26
+ /** Rebuilds now (the watcher also rebuilds on change). */
27
+ rebuild(): Promise<DevBuildResult>;
28
+ /** Stops the watcher and the HTTP server. */
29
+ close(): Promise<void>;
30
+ }
31
+ export interface DevBuildResult {
32
+ readonly ok: boolean;
33
+ /** Atoms compiled in this build. */
34
+ readonly compiled: number;
35
+ /** Compiler and validation diagnostics, already formatted for a terminal. */
36
+ readonly diagnostics: readonly string[];
37
+ readonly ms: number;
38
+ }
39
+ /** Implemented in ./server.ts. */
40
+ export type StartDevServer = (options: DevServerOptions) => Promise<DevServer>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The contract between `pwf dev` (./server.ts, written by the dev lane) and
3
+ * the rest of the CLI.
4
+ *
5
+ * `pwf dev` watches a pwpack folder, compiles its atoms with the engine's
6
+ * compiler, builds an envelope from the folder, and serves a page that boots
7
+ * the runner against it. When the app declares data or sign-in, it mounts a
8
+ * local host (../host) that implements the session and data protocols.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/dev/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * The contract between `pwf dev` (./server.ts, written by the dev lane) and\n * the rest of the CLI.\n *\n * `pwf dev` watches a pwpack folder, compiles its atoms with the engine's\n * compiler, builds an envelope from the folder, and serves a page that boots\n * the runner against it. When the app declares data or sign-in, it mounts a\n * local host (../host) that implements the session and data protocols.\n */\n\nimport type { LocalHost } from '../host/types.js'\n\nexport interface DevServerOptions {\n /** Absolute path of the pwpack folder to serve. */\n readonly root: string\n /** Port to listen on; 0 picks a free port. Default 4600. */\n readonly port?: number\n /** Host interface to bind. Default 127.0.0.1. */\n readonly hostname?: string\n /** Mount a local host for data and sign-in. Default: only when the app declares them. */\n readonly localHost?: boolean | LocalHost\n /** Called with human-readable progress lines. */\n readonly log?: (line: string) => void\n}\n\nexport interface DevServer {\n /** The URL the app is served at, e.g. http://127.0.0.1:4600/. */\n readonly url: string\n /** Rebuilds now (the watcher also rebuilds on change). */\n rebuild(): Promise<DevBuildResult>\n /** Stops the watcher and the HTTP server. */\n close(): Promise<void>\n}\n\nexport interface DevBuildResult {\n readonly ok: boolean\n /** Atoms compiled in this build. */\n readonly compiled: number\n /** Compiler and validation diagnostics, already formatted for a terminal. */\n readonly diagnostics: readonly string[]\n readonly ms: number\n}\n\n/** Implemented in ./server.ts. */\nexport type StartDevServer = (options: DevServerOptions) => Promise<DevServer>\n"]}
@@ -0,0 +1,29 @@
1
+ export type WatchMode = 'auto' | 'native' | 'poll';
2
+ export interface WatchOptions {
3
+ /** Default `auto`: native recursive watching, polling when it is unavailable. */
4
+ readonly mode?: WatchMode;
5
+ /** Quiet period before the callback fires. Default 150 ms. */
6
+ readonly debounceMs?: number;
7
+ /** Poll interval in `poll` mode. Default 500 ms. */
8
+ readonly pollMs?: number;
9
+ }
10
+ export interface Watcher {
11
+ /** `native` or `poll` — what is actually running. */
12
+ readonly mode: 'native' | 'poll';
13
+ close(): void;
14
+ }
15
+ /** Whether a change at `rel` (POSIX, relative to the root) should trigger a rebuild. */
16
+ export declare function isWatchedPath(rel: string): boolean;
17
+ /** A function that runs `fn` once `ms` after the last call. */
18
+ export declare function debounce(fn: () => void, ms: number): {
19
+ (): void;
20
+ cancel(): void;
21
+ };
22
+ /** A signature of every watched file (path, size, mtime), for polling. */
23
+ export declare function snapshot(root: string): Promise<string>;
24
+ /**
25
+ * Watches `root` and calls `onChange` after each debounced burst of relevant
26
+ * changes, with the POSIX paths that changed — or `null` when the platform
27
+ * could not say (polling, or an event without a file name).
28
+ */
29
+ export declare function watchFolder(root: string, onChange: (paths: string[] | null) => void, options?: WatchOptions): Watcher;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Watching an app folder for changes.
3
+ *
4
+ * `fs.watch` with `recursive: true` where the platform supports it (Windows,
5
+ * macOS, Linux on current Node); a polling walk where it does not. Either way
6
+ * the caller sees one debounced callback per burst of changes, and changes in
7
+ * the build's own output (`.pwf/build-cache`, `.pwf/out`), version control
8
+ * and dependencies are ignored. The pack cache (`.pwf/packs`) is watched:
9
+ * adding a pack is a change the app should pick up.
10
+ */
11
+ import { watch as fsWatch } from 'node:fs';
12
+ import { readdir, stat } from 'node:fs/promises';
13
+ import { join, relative, sep } from 'node:path';
14
+ const IGNORED_SEGMENTS = new Set(['node_modules', '.git', 'dist']);
15
+ const IGNORED_PREFIXES = ['.pwf/build-cache', '.pwf/out'];
16
+ /** Whether a change at `rel` (POSIX, relative to the root) should trigger a rebuild. */
17
+ export function isWatchedPath(rel) {
18
+ const p = rel.split(sep).join('/').replace(/^\.\//, '');
19
+ if (!p)
20
+ return true;
21
+ if (IGNORED_PREFIXES.some((x) => p === x || p.startsWith(`${x}/`)))
22
+ return false;
23
+ return !p.split('/').some((s) => IGNORED_SEGMENTS.has(s));
24
+ }
25
+ /** A function that runs `fn` once `ms` after the last call. */
26
+ export function debounce(fn, ms) {
27
+ let timer = null;
28
+ const run = () => {
29
+ if (timer)
30
+ clearTimeout(timer);
31
+ timer = setTimeout(() => {
32
+ timer = null;
33
+ fn();
34
+ }, ms);
35
+ };
36
+ run.cancel = () => {
37
+ if (timer)
38
+ clearTimeout(timer);
39
+ timer = null;
40
+ };
41
+ return run;
42
+ }
43
+ /** A signature of every watched file (path, size, mtime), for polling. */
44
+ export async function snapshot(root) {
45
+ const parts = [];
46
+ const walk = async (dir) => {
47
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
48
+ for (const e of entries) {
49
+ const full = join(dir, e.name);
50
+ const rel = relative(root, full);
51
+ if (!isWatchedPath(rel))
52
+ continue;
53
+ if (e.isDirectory())
54
+ await walk(full);
55
+ else if (e.isFile()) {
56
+ const st = await stat(full).catch(() => null);
57
+ if (st)
58
+ parts.push(`${rel}|${st.size}|${st.mtimeMs}`);
59
+ }
60
+ }
61
+ };
62
+ await walk(root);
63
+ return parts.sort().join('\n');
64
+ }
65
+ function startPolling(root, onChange, pollMs) {
66
+ let last = null;
67
+ let closed = false;
68
+ let running = false;
69
+ const tick = async () => {
70
+ if (running || closed)
71
+ return;
72
+ running = true;
73
+ try {
74
+ const now = await snapshot(root);
75
+ if (last !== null && now !== last && !closed)
76
+ onChange();
77
+ last = now;
78
+ }
79
+ finally {
80
+ running = false;
81
+ }
82
+ };
83
+ void tick();
84
+ const timer = setInterval(() => void tick(), pollMs);
85
+ return {
86
+ mode: 'poll',
87
+ close() {
88
+ closed = true;
89
+ clearInterval(timer);
90
+ },
91
+ };
92
+ }
93
+ /**
94
+ * Watches `root` and calls `onChange` after each debounced burst of relevant
95
+ * changes, with the POSIX paths that changed — or `null` when the platform
96
+ * could not say (polling, or an event without a file name).
97
+ */
98
+ export function watchFolder(root, onChange, options = {}) {
99
+ let changed = new Set();
100
+ const fire = debounce(() => {
101
+ const paths = changed ? [...changed].sort() : null;
102
+ changed = new Set();
103
+ onChange(paths);
104
+ }, options.debounceMs ?? 150);
105
+ const note = (path) => {
106
+ if (path === null)
107
+ changed = null;
108
+ else
109
+ changed?.add(path.split(sep).join('/'));
110
+ fire();
111
+ };
112
+ const mode = options.mode ?? 'auto';
113
+ if (mode !== 'poll') {
114
+ let watcher = null;
115
+ try {
116
+ watcher = fsWatch(root, { recursive: true }, (_event, filename) => {
117
+ if (filename === null)
118
+ note(null);
119
+ else if (isWatchedPath(String(filename)))
120
+ note(String(filename));
121
+ });
122
+ }
123
+ catch (e) {
124
+ if (mode === 'native')
125
+ throw e;
126
+ }
127
+ if (watcher) {
128
+ const w = watcher;
129
+ // A watcher that dies (the folder is removed, the platform gives up)
130
+ // must not take the server down; the next edit simply is not seen.
131
+ w.on('error', () => undefined);
132
+ return {
133
+ mode: 'native',
134
+ close() {
135
+ fire.cancel();
136
+ w.close();
137
+ },
138
+ };
139
+ }
140
+ }
141
+ const poller = startPolling(root, () => note(null), options.pollMs ?? 500);
142
+ return {
143
+ mode: 'poll',
144
+ close() {
145
+ fire.cancel();
146
+ poller.close();
147
+ },
148
+ };
149
+ }
150
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/dev/watcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,IAAI,OAAO,EAAkB,MAAM,SAAS,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAmB/C,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAClE,MAAM,gBAAgB,GAAG,CAAC,kBAAkB,EAAE,UAAU,CAAC,CAAA;AAEzD,wFAAwF;AACxF,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IACvD,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAA;IACnB,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IAChF,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;AAC3D,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,QAAQ,CAAC,EAAc,EAAE,EAAU;IACjD,IAAI,KAAK,GAAyC,IAAI,CAAA;IACtD,MAAM,GAAG,GAAG,GAAS,EAAE;QACrB,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAA;QAC9B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,KAAK,GAAG,IAAI,CAAA;YACZ,EAAE,EAAE,CAAA;QACN,CAAC,EAAE,EAAE,CAAC,CAAA;IACR,CAAC,CAAA;IACD,GAAG,CAAC,MAAM,GAAG,GAAS,EAAE;QACtB,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAA;QAC9B,KAAK,GAAG,IAAI,CAAA;IACd,CAAC,CAAA;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAY;IACzC,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,IAAI,GAAG,KAAK,EAAE,GAAW,EAAiB,EAAE;QAChD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;QAC3E,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAA;YAC9B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;YAChC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC;gBAAE,SAAQ;YACjC,IAAI,CAAC,CAAC,WAAW,EAAE;gBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA;iBAChC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;gBACpB,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;gBAC7C,IAAI,EAAE;oBAAE,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;YACvD,CAAC;QACH,CAAC;IACH,CAAC,CAAA;IACD,MAAM,IAAI,CAAC,IAAI,CAAC,CAAA;IAChB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAChC,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,QAAoB,EAAE,MAAc;IACtE,IAAI,IAAI,GAAkB,IAAI,CAAA;IAC9B,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;QACrC,IAAI,OAAO,IAAI,MAAM;YAAE,OAAM;QAC7B,OAAO,GAAG,IAAI,CAAA;QACd,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAA;YAChC,IAAI,IAAI,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,MAAM;gBAAE,QAAQ,EAAE,CAAA;YACxD,IAAI,GAAG,GAAG,CAAA;QACZ,CAAC;gBAAS,CAAC;YACT,OAAO,GAAG,KAAK,CAAA;QACjB,CAAC;IACH,CAAC,CAAA;IACD,KAAK,IAAI,EAAE,CAAA;IACX,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,MAAM,CAAC,CAAA;IACpD,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,KAAK;YACH,MAAM,GAAG,IAAI,CAAA;YACb,aAAa,CAAC,KAAK,CAAC,CAAA;QACtB,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,QAA0C,EAAE,UAAwB,EAAE;IAC9G,IAAI,OAAO,GAAuB,IAAI,GAAG,EAAE,CAAA;IAC3C,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,EAAE;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;QAClD,OAAO,GAAG,IAAI,GAAG,EAAE,CAAA;QACnB,QAAQ,CAAC,KAAK,CAAC,CAAA;IACjB,CAAC,EAAE,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC,CAAA;IAC7B,MAAM,IAAI,GAAG,CAAC,IAAmB,EAAQ,EAAE;QACzC,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,GAAG,IAAI,CAAA;;YAC5B,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;QAC5C,IAAI,EAAE,CAAA;IACR,CAAC,CAAA;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,MAAM,CAAA;IACnC,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,IAAI,OAAO,GAAqB,IAAI,CAAA;QACpC,IAAI,CAAC;YACH,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;gBAChE,IAAI,QAAQ,KAAK,IAAI;oBAAE,IAAI,CAAC,IAAI,CAAC,CAAA;qBAC5B,IAAI,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;oBAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAA;YAClE,CAAC,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,IAAI,KAAK,QAAQ;gBAAE,MAAM,CAAC,CAAA;QAChC,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,OAAO,CAAA;YACjB,qEAAqE;YACrE,mEAAmE;YACnE,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;YAC9B,OAAO;gBACL,IAAI,EAAE,QAAQ;gBACd,KAAK;oBACH,IAAI,CAAC,MAAM,EAAE,CAAA;oBACb,CAAC,CAAC,KAAK,EAAE,CAAA;gBACX,CAAC;aACF,CAAA;QACH,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,CAAA;IAC1E,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,KAAK;YACH,IAAI,CAAC,MAAM,EAAE,CAAA;YACb,MAAM,CAAC,KAAK,EAAE,CAAA;QAChB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * Watching an app folder for changes.\n *\n * `fs.watch` with `recursive: true` where the platform supports it (Windows,\n * macOS, Linux on current Node); a polling walk where it does not. Either way\n * the caller sees one debounced callback per burst of changes, and changes in\n * the build's own output (`.pwf/build-cache`, `.pwf/out`), version control\n * and dependencies are ignored. The pack cache (`.pwf/packs`) is watched:\n * adding a pack is a change the app should pick up.\n */\nimport { watch as fsWatch, type FSWatcher } from 'node:fs'\nimport { readdir, stat } from 'node:fs/promises'\nimport { join, relative, sep } from 'node:path'\n\nexport type WatchMode = 'auto' | 'native' | 'poll'\n\nexport interface WatchOptions {\n /** Default `auto`: native recursive watching, polling when it is unavailable. */\n readonly mode?: WatchMode\n /** Quiet period before the callback fires. Default 150 ms. */\n readonly debounceMs?: number\n /** Poll interval in `poll` mode. Default 500 ms. */\n readonly pollMs?: number\n}\n\nexport interface Watcher {\n /** `native` or `poll` — what is actually running. */\n readonly mode: 'native' | 'poll'\n close(): void\n}\n\nconst IGNORED_SEGMENTS = new Set(['node_modules', '.git', 'dist'])\nconst IGNORED_PREFIXES = ['.pwf/build-cache', '.pwf/out']\n\n/** Whether a change at `rel` (POSIX, relative to the root) should trigger a rebuild. */\nexport function isWatchedPath(rel: string): boolean {\n const p = rel.split(sep).join('/').replace(/^\\.\\//, '')\n if (!p) return true\n if (IGNORED_PREFIXES.some((x) => p === x || p.startsWith(`${x}/`))) return false\n return !p.split('/').some((s) => IGNORED_SEGMENTS.has(s))\n}\n\n/** A function that runs `fn` once `ms` after the last call. */\nexport function debounce(fn: () => void, ms: number): { (): void; cancel(): void } {\n let timer: ReturnType<typeof setTimeout> | null = null\n const run = (): void => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = null\n fn()\n }, ms)\n }\n run.cancel = (): void => {\n if (timer) clearTimeout(timer)\n timer = null\n }\n return run\n}\n\n/** A signature of every watched file (path, size, mtime), for polling. */\nexport async function snapshot(root: string): Promise<string> {\n const parts: string[] = []\n const walk = async (dir: string): Promise<void> => {\n const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])\n for (const e of entries) {\n const full = join(dir, e.name)\n const rel = relative(root, full)\n if (!isWatchedPath(rel)) continue\n if (e.isDirectory()) await walk(full)\n else if (e.isFile()) {\n const st = await stat(full).catch(() => null)\n if (st) parts.push(`${rel}|${st.size}|${st.mtimeMs}`)\n }\n }\n }\n await walk(root)\n return parts.sort().join('\\n')\n}\n\nfunction startPolling(root: string, onChange: () => void, pollMs: number): Watcher {\n let last: string | null = null\n let closed = false\n let running = false\n const tick = async (): Promise<void> => {\n if (running || closed) return\n running = true\n try {\n const now = await snapshot(root)\n if (last !== null && now !== last && !closed) onChange()\n last = now\n } finally {\n running = false\n }\n }\n void tick()\n const timer = setInterval(() => void tick(), pollMs)\n return {\n mode: 'poll',\n close() {\n closed = true\n clearInterval(timer)\n },\n }\n}\n\n/**\n * Watches `root` and calls `onChange` after each debounced burst of relevant\n * changes, with the POSIX paths that changed — or `null` when the platform\n * could not say (polling, or an event without a file name).\n */\nexport function watchFolder(root: string, onChange: (paths: string[] | null) => void, options: WatchOptions = {}): Watcher {\n let changed: Set<string> | null = new Set()\n const fire = debounce(() => {\n const paths = changed ? [...changed].sort() : null\n changed = new Set()\n onChange(paths)\n }, options.debounceMs ?? 150)\n const note = (path: string | null): void => {\n if (path === null) changed = null\n else changed?.add(path.split(sep).join('/'))\n fire()\n }\n const mode = options.mode ?? 'auto'\n if (mode !== 'poll') {\n let watcher: FSWatcher | null = null\n try {\n watcher = fsWatch(root, { recursive: true }, (_event, filename) => {\n if (filename === null) note(null)\n else if (isWatchedPath(String(filename))) note(String(filename))\n })\n } catch (e) {\n if (mode === 'native') throw e\n }\n if (watcher) {\n const w = watcher\n // A watcher that dies (the folder is removed, the platform gives up)\n // must not take the server down; the next edit simply is not seen.\n w.on('error', () => undefined)\n return {\n mode: 'native',\n close() {\n fire.cancel()\n w.close()\n },\n }\n }\n }\n const poller = startPolling(root, () => note(null), options.pollMs ?? 500)\n return {\n mode: 'poll',\n close() {\n fire.cancel()\n poller.close()\n },\n }\n}\n"]}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * What the local host reads from the pwpack folder.
3
+ *
4
+ * app/definitions/auth.json who may enter (definitions.md, `auth.json`)
5
+ * app/definitions/entities.json the collections the host keeps
6
+ * app/surfaces/<key>.receipt.json the pages it serves
7
+ * app/data/<typeId>.jsonl | .json seed rows (a convention of this host)
8
+ *
9
+ * Read on every call, never cached: `pwf dev` edits these files while the
10
+ * host runs, and a definition change must apply without a restart. An absent
11
+ * file is "not declared"; an unreadable one is reported as an error rather
12
+ * than guessed around.
13
+ */
14
+ export interface AuthDeclaration {
15
+ readonly required: boolean;
16
+ readonly topology: 'app' | 'external';
17
+ readonly methods: readonly string[];
18
+ readonly issuer?: {
19
+ readonly issuer: string;
20
+ readonly clientId: string;
21
+ readonly scopes: readonly string[];
22
+ readonly audience?: string;
23
+ };
24
+ readonly access: {
25
+ readonly rules: readonly {
26
+ readonly subject: string;
27
+ readonly model: string;
28
+ readonly rule: string;
29
+ }[];
30
+ };
31
+ }
32
+ export interface EntityField {
33
+ readonly name: string;
34
+ readonly type: string;
35
+ readonly required: boolean;
36
+ }
37
+ export interface EntityDeclaration {
38
+ readonly typeId: string;
39
+ readonly displayName?: string;
40
+ readonly fields: readonly EntityField[];
41
+ /** Carried member (not in the spec yet): `"owner"` makes the collection per-visitor. */
42
+ readonly scope?: string;
43
+ }
44
+ /** `undefined` when the app declares no `auth.json` (or an empty one). */
45
+ export declare function readAuth(root: string): AuthDeclaration | undefined;
46
+ export declare function readEntities(root: string): EntityDeclaration[];
47
+ /** A page key is a file name, never a path. */
48
+ export declare const PAGE_KEY: RegExp;
49
+ export declare function surfaceKeys(root: string): string[];
50
+ export declare function readReceipt(root: string, key: string): Record<string, unknown> | undefined;
51
+ /**
52
+ * Seed rows for one collection.
53
+ *
54
+ * THE CONVENTION (proposed for the spec): `app/data/<typeId>.json` holding
55
+ * `{ "data": [ …records ] }` — the same `data` envelope a record list answer
56
+ * uses. A record may carry its own `id`; `createdAt`/`updatedAt` are filled
57
+ * when absent. Also accepted, for hand-written seed files only:
58
+ * `app/data/<typeId>.jsonl`, one JSON object per line (blank lines skipped),
59
+ * read when no `.json` file exists.
60
+ */
61
+ export declare function readSeed(root: string, typeId: string): Record<string, unknown>[];