@webstir-io/webstir-frontend 0.1.40

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 (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/assets/assetManifest.d.ts +16 -0
  4. package/dist/assets/assetManifest.js +31 -0
  5. package/dist/assets/imageOptimizer.d.ts +6 -0
  6. package/dist/assets/imageOptimizer.js +93 -0
  7. package/dist/assets/precompression.d.ts +1 -0
  8. package/dist/assets/precompression.js +21 -0
  9. package/dist/builders/contentBuilder.d.ts +2 -0
  10. package/dist/builders/contentBuilder.js +1052 -0
  11. package/dist/builders/cssBuilder.d.ts +2 -0
  12. package/dist/builders/cssBuilder.js +439 -0
  13. package/dist/builders/htmlBuilder.d.ts +2 -0
  14. package/dist/builders/htmlBuilder.js +430 -0
  15. package/dist/builders/index.d.ts +2 -0
  16. package/dist/builders/index.js +14 -0
  17. package/dist/builders/jsBuilder.d.ts +2 -0
  18. package/dist/builders/jsBuilder.js +300 -0
  19. package/dist/builders/staticAssetsBuilder.d.ts +2 -0
  20. package/dist/builders/staticAssetsBuilder.js +158 -0
  21. package/dist/builders/types.d.ts +12 -0
  22. package/dist/builders/types.js +1 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +105 -0
  25. package/dist/config/manifest.d.ts +7 -0
  26. package/dist/config/manifest.js +17 -0
  27. package/dist/config/paths.d.ts +3 -0
  28. package/dist/config/paths.js +11 -0
  29. package/dist/config/schema.d.ts +413 -0
  30. package/dist/config/schema.js +44 -0
  31. package/dist/config/setup.d.ts +2 -0
  32. package/dist/config/setup.js +12 -0
  33. package/dist/config/workspace.d.ts +2 -0
  34. package/dist/config/workspace.js +131 -0
  35. package/dist/config/workspaceManifest.d.ts +23 -0
  36. package/dist/config/workspaceManifest.js +1 -0
  37. package/dist/core/constants.d.ts +70 -0
  38. package/dist/core/constants.js +70 -0
  39. package/dist/core/diagnostics.d.ts +15 -0
  40. package/dist/core/diagnostics.js +21 -0
  41. package/dist/core/index.d.ts +3 -0
  42. package/dist/core/index.js +3 -0
  43. package/dist/core/pages.d.ts +6 -0
  44. package/dist/core/pages.js +23 -0
  45. package/dist/hooks.d.ts +19 -0
  46. package/dist/hooks.js +115 -0
  47. package/dist/html/criticalCss.d.ts +4 -0
  48. package/dist/html/criticalCss.js +192 -0
  49. package/dist/html/htmlSecurity.d.ts +5 -0
  50. package/dist/html/htmlSecurity.js +73 -0
  51. package/dist/html/lazyLoad.d.ts +6 -0
  52. package/dist/html/lazyLoad.js +21 -0
  53. package/dist/html/pageScaffold.d.ts +10 -0
  54. package/dist/html/pageScaffold.js +51 -0
  55. package/dist/html/resourceHints.d.ts +7 -0
  56. package/dist/html/resourceHints.js +64 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/modes/ssg/index.d.ts +4 -0
  60. package/dist/modes/ssg/index.js +4 -0
  61. package/dist/modes/ssg/metadata.d.ts +5 -0
  62. package/dist/modes/ssg/metadata.js +50 -0
  63. package/dist/modes/ssg/routing.d.ts +2 -0
  64. package/dist/modes/ssg/routing.js +186 -0
  65. package/dist/modes/ssg/seo.d.ts +4 -0
  66. package/dist/modes/ssg/seo.js +208 -0
  67. package/dist/modes/ssg/validation.d.ts +3 -0
  68. package/dist/modes/ssg/validation.js +27 -0
  69. package/dist/modes/ssg/views.d.ts +2 -0
  70. package/dist/modes/ssg/views.js +236 -0
  71. package/dist/operations.d.ts +5 -0
  72. package/dist/operations.js +102 -0
  73. package/dist/pipeline.d.ts +7 -0
  74. package/dist/pipeline.js +71 -0
  75. package/dist/provider.d.ts +2 -0
  76. package/dist/provider.js +176 -0
  77. package/dist/types.d.ts +61 -0
  78. package/dist/types.js +1 -0
  79. package/dist/utils/changedFile.d.ts +8 -0
  80. package/dist/utils/changedFile.js +26 -0
  81. package/dist/utils/fs.d.ts +11 -0
  82. package/dist/utils/fs.js +39 -0
  83. package/dist/utils/hash.d.ts +1 -0
  84. package/dist/utils/hash.js +5 -0
  85. package/dist/utils/pagePaths.d.ts +5 -0
  86. package/dist/utils/pagePaths.js +36 -0
  87. package/dist/utils/pathMatch.d.ts +3 -0
  88. package/dist/utils/pathMatch.js +29 -0
  89. package/dist/watch/frontendFiles.d.ts +3 -0
  90. package/dist/watch/frontendFiles.js +25 -0
  91. package/dist/watch/hotUpdateTracker.d.ts +51 -0
  92. package/dist/watch/hotUpdateTracker.js +205 -0
  93. package/dist/watch/pipelineHelpers.d.ts +26 -0
  94. package/dist/watch/pipelineHelpers.js +177 -0
  95. package/dist/watch/types.d.ts +27 -0
  96. package/dist/watch/types.js +1 -0
  97. package/dist/watch/watchCoordinator.d.ts +36 -0
  98. package/dist/watch/watchCoordinator.js +551 -0
  99. package/dist/watch/watchDaemon.d.ts +17 -0
  100. package/dist/watch/watchDaemon.js +127 -0
  101. package/dist/watch/watchReporter.d.ts +21 -0
  102. package/dist/watch/watchReporter.js +64 -0
  103. package/package.json +92 -0
  104. package/scripts/publish.sh +101 -0
  105. package/scripts/smoke.mjs +35 -0
  106. package/scripts/update-contract.sh +121 -0
  107. package/src/assets/assetManifest.ts +51 -0
  108. package/src/assets/imageOptimizer.ts +112 -0
  109. package/src/assets/precompression.ts +25 -0
  110. package/src/builders/contentBuilder.ts +1400 -0
  111. package/src/builders/cssBuilder.ts +552 -0
  112. package/src/builders/htmlBuilder.ts +540 -0
  113. package/src/builders/index.ts +16 -0
  114. package/src/builders/jsBuilder.ts +358 -0
  115. package/src/builders/staticAssetsBuilder.ts +174 -0
  116. package/src/builders/types.ts +15 -0
  117. package/src/cli.ts +108 -0
  118. package/src/config/manifest.ts +24 -0
  119. package/src/config/paths.ts +14 -0
  120. package/src/config/schema.ts +49 -0
  121. package/src/config/setup.ts +14 -0
  122. package/src/config/workspace.ts +150 -0
  123. package/src/config/workspaceManifest.ts +27 -0
  124. package/src/core/constants.ts +73 -0
  125. package/src/core/diagnostics.ts +40 -0
  126. package/src/core/index.ts +3 -0
  127. package/src/core/pages.ts +31 -0
  128. package/src/hooks.ts +175 -0
  129. package/src/html/criticalCss.ts +214 -0
  130. package/src/html/htmlSecurity.ts +86 -0
  131. package/src/html/lazyLoad.ts +30 -0
  132. package/src/html/pageScaffold.ts +70 -0
  133. package/src/html/resourceHints.ts +91 -0
  134. package/src/index.ts +5 -0
  135. package/src/modes/ssg/index.ts +4 -0
  136. package/src/modes/ssg/metadata.ts +63 -0
  137. package/src/modes/ssg/routing.ts +230 -0
  138. package/src/modes/ssg/seo.ts +261 -0
  139. package/src/modes/ssg/validation.ts +37 -0
  140. package/src/modes/ssg/views.ts +309 -0
  141. package/src/operations.ts +138 -0
  142. package/src/pipeline.ts +88 -0
  143. package/src/provider.ts +249 -0
  144. package/src/types.ts +67 -0
  145. package/src/utils/changedFile.ts +39 -0
  146. package/src/utils/fs.ts +48 -0
  147. package/src/utils/hash.ts +6 -0
  148. package/src/utils/pagePaths.ts +43 -0
  149. package/src/utils/pathMatch.ts +36 -0
  150. package/src/watch/frontendFiles.ts +32 -0
  151. package/src/watch/hotUpdateTracker.ts +285 -0
  152. package/src/watch/pipelineHelpers.ts +242 -0
  153. package/src/watch/types.ts +23 -0
  154. package/src/watch/watchCoordinator.ts +666 -0
  155. package/src/watch/watchDaemon.ts +144 -0
  156. package/src/watch/watchReporter.ts +98 -0
  157. package/tests/add-page-defaults.test.js +64 -0
  158. package/tests/content-pages.test.js +81 -0
  159. package/tests/css-app-imports.test.js +64 -0
  160. package/tests/css-page-imports.test.js +100 -0
  161. package/tests/diagnostics.test.js +48 -0
  162. package/tests/features.test.js +63 -0
  163. package/tests/hooks.test.js +71 -0
  164. package/tests/provider.integration.test.js +137 -0
  165. package/tests/ssg-defaults.test.js +201 -0
  166. package/tests/ssg-guardrails.test.js +69 -0
  167. package/tsconfig.json +27 -0
@@ -0,0 +1,127 @@
1
+ import process from 'node:process';
2
+ import { createInterface } from 'node:readline';
3
+ import { emitDiagnostic } from '../core/diagnostics.js';
4
+ import { WatchCoordinator } from './watchCoordinator.js';
5
+ export class WatchDaemon {
6
+ coordinator;
7
+ options;
8
+ shutdownPromise;
9
+ resolveShutdown = null;
10
+ commandQueue = Promise.resolve();
11
+ isShuttingDown = false;
12
+ rl;
13
+ constructor(options) {
14
+ this.options = options;
15
+ this.coordinator = new WatchCoordinator({
16
+ workspaceRoot: options.workspaceRoot,
17
+ verbose: options.verbose ?? false,
18
+ hmrVerbose: options.hmrVerbose ?? false
19
+ });
20
+ this.shutdownPromise = new Promise((resolve) => {
21
+ this.resolveShutdown = resolve;
22
+ });
23
+ }
24
+ async run() {
25
+ if (this.options.autoStart !== false) {
26
+ await this.coordinator.start();
27
+ }
28
+ this.setupSignalHandlers();
29
+ this.setupCommandLoop();
30
+ await this.shutdownPromise;
31
+ }
32
+ setupCommandLoop() {
33
+ if (process.stdin.isTTY) {
34
+ process.stdin.setRawMode(false);
35
+ }
36
+ process.stdin.setEncoding('utf8');
37
+ this.rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
38
+ this.rl.on('line', (line) => this.processLine(line));
39
+ this.rl.on('close', () => {
40
+ void this.shutdown();
41
+ });
42
+ }
43
+ setupSignalHandlers() {
44
+ const shutdown = () => {
45
+ void this.shutdown();
46
+ };
47
+ process.on('SIGINT', shutdown);
48
+ process.on('SIGTERM', shutdown);
49
+ }
50
+ processLine(rawLine) {
51
+ const line = rawLine.trim();
52
+ if (line.length === 0) {
53
+ return;
54
+ }
55
+ let command = null;
56
+ try {
57
+ command = JSON.parse(line);
58
+ }
59
+ catch (error) {
60
+ emitDiagnostic({
61
+ code: 'frontend.watch.command.invalid',
62
+ kind: 'watch-daemon',
63
+ stage: 'command',
64
+ severity: 'warning',
65
+ message: `Discarding invalid command payload: ${String(error)}`
66
+ });
67
+ return;
68
+ }
69
+ this.commandQueue = this.commandQueue.then(() => this.handleCommand(command)).catch((error) => {
70
+ emitDiagnostic({
71
+ code: 'frontend.watch.command.failure',
72
+ kind: 'watch-daemon',
73
+ stage: 'command',
74
+ severity: 'error',
75
+ message: `Command handling failed: ${error instanceof Error ? error.message : String(error)}`
76
+ });
77
+ });
78
+ }
79
+ async handleCommand(command) {
80
+ switch (command.type) {
81
+ case 'start':
82
+ await this.coordinator.start();
83
+ return;
84
+ case 'reload':
85
+ await this.coordinator.reload();
86
+ return;
87
+ case 'change':
88
+ await this.coordinator.handleChange({ path: command.path });
89
+ return;
90
+ case 'shutdown':
91
+ await this.shutdown();
92
+ return;
93
+ case 'ping':
94
+ emitDiagnostic({
95
+ code: 'frontend.watch.pong',
96
+ kind: 'watch-daemon',
97
+ stage: 'command',
98
+ severity: 'info',
99
+ message: 'Watch daemon heartbeat acknowledged.',
100
+ data: command.id ? { id: command.id } : undefined
101
+ });
102
+ return;
103
+ default:
104
+ emitDiagnostic({
105
+ code: 'frontend.watch.command.unknown',
106
+ kind: 'watch-daemon',
107
+ stage: 'command',
108
+ severity: 'warning',
109
+ message: `Unknown watch daemon command: ${command.type}`
110
+ });
111
+ return;
112
+ }
113
+ }
114
+ async shutdown() {
115
+ if (this.isShuttingDown) {
116
+ return;
117
+ }
118
+ this.isShuttingDown = true;
119
+ if (this.rl) {
120
+ this.rl.close();
121
+ this.rl = undefined;
122
+ }
123
+ await this.coordinator.stop();
124
+ this.resolveShutdown?.();
125
+ this.resolveShutdown = null;
126
+ }
127
+ }
@@ -0,0 +1,21 @@
1
+ import type { BuildResult, Message } from 'esbuild';
2
+ import type { DiagnosticEvent } from '../core/diagnostics.js';
3
+ export interface WatchReporterOptions {
4
+ readonly verbose: boolean;
5
+ }
6
+ export interface SerializedMessage {
7
+ readonly text: string;
8
+ readonly location?: {
9
+ readonly file?: string;
10
+ readonly line?: number;
11
+ readonly column?: number;
12
+ };
13
+ }
14
+ export declare class WatchReporter {
15
+ private readonly verbose;
16
+ constructor(options: WatchReporterOptions);
17
+ emit(event: DiagnosticEvent): void;
18
+ emitVerbose(event: DiagnosticEvent): void;
19
+ emitJavaScriptStats(pageName: string, result: BuildResult, durationMs: number): void;
20
+ }
21
+ export declare function serializeMessages(messages: readonly Message[]): SerializedMessage[];
@@ -0,0 +1,64 @@
1
+ import { emitDiagnostic } from '../core/diagnostics.js';
2
+ export class WatchReporter {
3
+ verbose;
4
+ constructor(options) {
5
+ this.verbose = options.verbose;
6
+ }
7
+ emit(event) {
8
+ emitDiagnostic(event);
9
+ }
10
+ emitVerbose(event) {
11
+ if (!this.verbose) {
12
+ return;
13
+ }
14
+ emitDiagnostic(event);
15
+ }
16
+ emitJavaScriptStats(pageName, result, durationMs) {
17
+ if (!this.verbose) {
18
+ return;
19
+ }
20
+ const stats = extractMetafileStats(result);
21
+ const data = {
22
+ page: pageName,
23
+ durationMs: Number(durationMs.toFixed(1))
24
+ };
25
+ if (stats) {
26
+ data.inputs = stats.inputs;
27
+ data.outputs = stats.outputs;
28
+ data.bytes = stats.bytes;
29
+ }
30
+ emitDiagnostic({
31
+ code: 'frontend.watch.javascript.build.stats',
32
+ kind: 'watch-daemon',
33
+ stage: 'javascript',
34
+ severity: 'info',
35
+ message: `JavaScript rebuild stats for '${pageName}' (${durationMs.toFixed(1)}ms).`,
36
+ data
37
+ });
38
+ }
39
+ }
40
+ export function serializeMessages(messages) {
41
+ return messages.map((message) => ({
42
+ text: message.text,
43
+ location: message.location
44
+ ? {
45
+ file: message.location.file,
46
+ line: message.location.line,
47
+ column: message.location.column
48
+ }
49
+ : undefined
50
+ }));
51
+ }
52
+ function extractMetafileStats(result) {
53
+ if (!result.metafile) {
54
+ return null;
55
+ }
56
+ const inputs = Object.keys(result.metafile.inputs ?? {}).length;
57
+ const outputsEntries = Object.entries(result.metafile.outputs ?? {});
58
+ const bytes = outputsEntries.reduce((sum, [, output]) => sum + (output.bytes ?? 0), 0);
59
+ return {
60
+ inputs,
61
+ outputs: outputsEntries.length,
62
+ bytes
63
+ };
64
+ }
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@webstir-io/webstir-frontend",
3
+ "version": "0.1.40",
4
+ "description": "Frontend build and publish tooling for Webstir workspaces.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./core": {
15
+ "types": "./dist/core/index.d.ts",
16
+ "import": "./dist/core/index.js",
17
+ "default": "./dist/core/index.js"
18
+ },
19
+ "./builders": {
20
+ "types": "./dist/builders/index.d.ts",
21
+ "import": "./dist/builders/index.js",
22
+ "default": "./dist/builders/index.js"
23
+ },
24
+ "./cli": {
25
+ "import": "./dist/cli.js",
26
+ "default": "./dist/cli.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "bin": {
31
+ "webstir-frontend": "dist/cli.js"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "clean": "rm -rf dist",
36
+ "test": "node -e \"const { spawnSync } = require('node:child_process'); const glob = require('glob'); const files = glob.sync('tests/**/*.test.js'); if (!files.length) { console.error('No frontend test files found'); process.exit(1); } const env = { ...process.env, BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA: 'true', BROWSERSLIST_IGNORE_OLD_DATA: 'true' }; const result = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit', env }); process.exit(result.status ?? 1);\"",
37
+ "prepare": "npm run build",
38
+ "smoke": "npm run build && node scripts/smoke.mjs",
39
+ "release": "bash scripts/publish.sh"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src",
44
+ "scripts",
45
+ "tests",
46
+ "tsconfig.json",
47
+ "package-lock.json"
48
+ ],
49
+ "engines": {
50
+ "node": ">=20.18.1"
51
+ },
52
+ "license": "MIT",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "https://github.com/webstir-io/webstir-frontend"
56
+ },
57
+ "keywords": [
58
+ "webstir",
59
+ "frontend",
60
+ "cli"
61
+ ],
62
+ "author": "Webstir",
63
+ "publishConfig": {
64
+ "registry": "https://registry.npmjs.org",
65
+ "access": "public"
66
+ },
67
+ "dependencies": {
68
+ "@webstir-io/module-contract": "^0.1.13",
69
+ "autoprefixer": "^10.4.18",
70
+ "cheerio": "^1.0.0-rc.12",
71
+ "commander": "^12.1.0",
72
+ "csso": "^5.0.5",
73
+ "esbuild": "^0.25.10",
74
+ "fs-extra": "^11.2.0",
75
+ "glob": "^10.4.1",
76
+ "highlight.js": "^11.11.1",
77
+ "html-minifier-terser": "^7.2.0",
78
+ "marked": "^12.0.2",
79
+ "postcss": "^8.4.47",
80
+ "postcss-custom-media": "^11.0.6",
81
+ "sharp": "^0.33.3",
82
+ "zod": "^3.23.8"
83
+ },
84
+ "devDependencies": {
85
+ "@types/csso": "^5.0.4",
86
+ "@types/fs-extra": "^11.0.4",
87
+ "@types/glob": "^8.1.0",
88
+ "@types/html-minifier-terser": "^7.0.2",
89
+ "@types/node": "^20.11.25",
90
+ "typescript": "^5.7.2"
91
+ }
92
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ usage() {
6
+ cat <<'EOF'
7
+ Usage: scripts/publish.sh <patch|minor|major|x.y.z> [--no-push]
8
+
9
+ Examples:
10
+ scripts/publish.sh patch
11
+ scripts/publish.sh 0.1.0
12
+
13
+ The script requires a clean git worktree and npm publish access to
14
+ @webstir-io. Publishing is handled by GitHub Actions via npm trusted
15
+ publishing (OIDC) after the version tag is pushed.
16
+
17
+ By default, the script pushes the version bump commit and tag. To skip pushing,
18
+ pass --no-push or set PUBLISH_NO_PUSH=1.
19
+ EOF
20
+ exit 1
21
+ }
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
25
+
26
+ main() {
27
+ if [[ $# -lt 1 ]]; then
28
+ echo "error: version bump argument missing" >&2
29
+ usage
30
+ fi
31
+
32
+ local bump="$1"; shift || true
33
+ local no_push="false"
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --no-push)
38
+ no_push="true"
39
+ ;;
40
+ *)
41
+ echo "error: unknown option '$1'" >&2
42
+ usage
43
+ ;;
44
+ esac
45
+ shift || true
46
+ done
47
+
48
+ if [[ ! $bump =~ ^(patch|minor|major)$ && ! $bump =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
49
+ echo "error: invalid bump '$bump'" >&2
50
+ usage
51
+ fi
52
+
53
+ ensure_clean_git
54
+
55
+ cd "$ROOT_DIR"
56
+
57
+ echo "› npm version $bump"
58
+ npm version "$bump" -m "v%s"
59
+
60
+ echo "› npm install --package-lock-only"
61
+ npm install --package-lock-only
62
+
63
+ echo "› npm run clean"
64
+ npm run clean
65
+
66
+ echo "› npm run build"
67
+ npm run build
68
+
69
+ echo "› npm test"
70
+ npm test
71
+
72
+ echo "› npm run smoke"
73
+ npm run smoke
74
+
75
+ echo "› Skipping direct npm publish; pushing commit+tag will trigger the release workflow."
76
+
77
+ if [[ "$no_push" == "true" || "${PUBLISH_NO_PUSH:-}" =~ ^([Yy][Ee][Ss]|[Yy]|1|true)$ ]]; then
78
+ echo "› Skipping git push (no-push)."
79
+ echo " To publish upstream later, run: git push && git push --tags"
80
+ return 0
81
+ fi
82
+
83
+ echo "› git push"
84
+ git push
85
+ echo "› git push --tags"
86
+ git push --tags
87
+ }
88
+
89
+ ensure_clean_git() {
90
+ cd "$ROOT_DIR"
91
+ if ! git diff --quiet --ignore-submodules HEAD; then
92
+ echo "error: git worktree has uncommitted changes" >&2
93
+ exit 1
94
+ fi
95
+ if ! git diff --quiet --cached --ignore-submodules; then
96
+ echo "error: git index has staged changes" >&2
97
+ exit 1
98
+ fi
99
+ }
100
+
101
+ main "$@"
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { frontendProvider } from '../dist/index.js';
5
+
6
+ async function createWorkspace() {
7
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-frontend-smoke-'));
8
+ const appDir = path.join(root, 'src', 'frontend', 'app');
9
+ const pageDir = path.join(root, 'src', 'frontend', 'pages', 'home');
10
+ await fs.mkdir(appDir, { recursive: true });
11
+ await fs.mkdir(pageDir, { recursive: true });
12
+ await fs.writeFile(path.join(appDir, 'app.html'), '<!DOCTYPE html><html><head><title>App</title></head><body><main></main></body></html>', 'utf8');
13
+ await fs.writeFile(path.join(pageDir, 'index.html'), '<head></head><main><section>Home</section></main>', 'utf8');
14
+ await fs.writeFile(path.join(pageDir, 'index.ts'), 'console.log("home")', 'utf8');
15
+ return root;
16
+ }
17
+
18
+ async function main() {
19
+ const workspace = await createWorkspace();
20
+ console.info('[smoke:frontend] build mode');
21
+ const build = await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'build' }, incremental: false });
22
+ console.info('[smoke:frontend] build entries:', build.manifest.entryPoints);
23
+ console.info('[smoke:frontend] build diagnostics:', build.manifest.diagnostics.map(d => d.message));
24
+
25
+ console.info('[smoke:frontend] publish mode');
26
+ const publish = await frontendProvider.build({ workspaceRoot: workspace, env: { WEBSTIR_MODULE_MODE: 'publish' }, incremental: false });
27
+ console.info('[smoke:frontend] publish entries:', publish.manifest.entryPoints);
28
+ console.info('[smoke:frontend] publish diagnostics:', publish.manifest.diagnostics.map(d => d.message));
29
+ }
30
+
31
+ main().catch((err) => {
32
+ console.error(err);
33
+ process.exit(1);
34
+ });
35
+
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ usage() {
6
+ cat <<'EOF'
7
+ Usage: scripts/update-contract.sh [x.y.z|--latest] [--exact] [--fast]
8
+
9
+ Updates @webstir-io/module-contract (defaults to latest when no version is provided),
10
+ installs deps, then builds and tests the frontend package. Does NOT publish. If
11
+ everything passes, run scripts/publish.sh <bump> separately.
12
+
13
+ Examples:
14
+ scripts/update-contract.sh # use latest
15
+ scripts/update-contract.sh --latest # explicit latest
16
+ scripts/update-contract.sh 0.1.9 # specific version (caret range)
17
+ scripts/update-contract.sh 0.1.9 --exact # set exact version instead of ^range
18
+ scripts/update-contract.sh 0.1.9 --fast # lockfile-only update; skip build/test
19
+ EOF
20
+ exit 1
21
+ }
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
25
+
26
+ has_script() {
27
+ local script_name="$1"
28
+ node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); const has=!!(pkg.scripts && Object.prototype.hasOwnProperty.call(pkg.scripts, '${script_name}')); process.exit(has ? 0 : 1);"
29
+ }
30
+
31
+ main() {
32
+ local ver=""
33
+ local exact="false"
34
+ local fast="false"
35
+
36
+ while [[ $# -gt 0 ]]; do
37
+ case "$1" in
38
+ --latest)
39
+ ver="__resolve_latest__"
40
+ ;;
41
+ --exact)
42
+ exact="true"
43
+ ;;
44
+ --fast)
45
+ fast="true"
46
+ ;;
47
+ -h|--help)
48
+ usage ;;
49
+ *)
50
+ if [[ -n "$ver" && "$ver" != "__resolve_latest__" ]]; then
51
+ echo "error: duplicate version argument '$1'" >&2
52
+ usage
53
+ fi
54
+ ver="$1"
55
+ ;;
56
+ esac
57
+ shift || true
58
+ done
59
+
60
+ if [[ -z "$ver" || "$ver" == "__resolve_latest__" ]]; then
61
+ echo "› Resolving latest @webstir-io/module-contract version"
62
+ ver="$(npm view @webstir-io/module-contract version 2>/dev/null || true)"
63
+ if [[ -z "$ver" ]]; then
64
+ echo "error: unable to resolve latest @webstir-io/module-contract version" >&2
65
+ exit 1
66
+ fi
67
+ fi
68
+
69
+ if [[ ! $ver =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
70
+ echo "error: invalid version '$ver' (expected x.y.z)" >&2
71
+ usage
72
+ fi
73
+
74
+ cd "$ROOT_DIR"
75
+
76
+ local spec
77
+ if [[ "$exact" == "true" ]]; then
78
+ spec="$ver"
79
+ else
80
+ spec="^$ver"
81
+ fi
82
+
83
+ echo "› Setting @webstir-io/module-contract to $spec"
84
+ npm pkg set "dependencies.@webstir-io/module-contract=$spec"
85
+
86
+ echo "› npm install (refresh lockfile)"
87
+ if [[ "$fast" == "true" ]]; then
88
+ npm install --package-lock-only --no-audit --no-fund --ignore-scripts
89
+ else
90
+ npm install --no-audit --no-fund
91
+ fi
92
+
93
+ local frontend_ver
94
+ frontend_ver="$(node -p "require('./package.json').version" 2>/dev/null || echo 'unknown')"
95
+ local installed_contract
96
+ installed_contract="$(npm ls @webstir-io/module-contract --json 2>/dev/null | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const j=JSON.parse(d);const v=(j.dependencies&&j.dependencies['@webstir-io/module-contract']&&j.dependencies['@webstir-io/module-contract'].version)||'';console.log(v||'unknown')}catch{console.log('unknown')}})")"
97
+ echo "› Frontend package: @webstir-io/webstir-frontend@${frontend_ver}"
98
+ echo "› Contract installed: @webstir-io/module-contract@${installed_contract}"
99
+
100
+ if [[ "$fast" != "true" ]]; then
101
+ if has_script build; then
102
+ echo "› npm run build"
103
+ npm run build
104
+ fi
105
+
106
+ if has_script test; then
107
+ echo "› npm test"
108
+ npm test
109
+ fi
110
+
111
+ if has_script smoke; then
112
+ echo "› npm run smoke"
113
+ npm run smoke
114
+ fi
115
+ fi
116
+
117
+ echo
118
+ echo "Contract update complete: @webstir-io/module-contract@$spec"
119
+ }
120
+
121
+ main "$@"
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { readJson, writeJson, ensureDir } from '../utils/fs.js';
3
+
4
+ export interface PageAssetManifest {
5
+ js?: string;
6
+ css?: string;
7
+ }
8
+
9
+ export interface AssetManifest {
10
+ pages: Record<string, PageAssetManifest>;
11
+ shared?: SharedAssets;
12
+ }
13
+
14
+ export interface SharedAssets {
15
+ css?: string;
16
+ js?: string;
17
+ }
18
+
19
+ const MANIFEST_FILENAME = 'manifest.json';
20
+
21
+ export async function updatePageManifest(directory: string, pageName: string, updater: (value: PageAssetManifest) => void): Promise<void> {
22
+ const manifestPath = path.join(directory, MANIFEST_FILENAME);
23
+ await ensureDir(directory);
24
+ const manifest = (await readJson<AssetManifest>(manifestPath)) ?? { pages: {} };
25
+ const pageManifest: PageAssetManifest = manifest.pages[pageName] ?? {};
26
+ updater(pageManifest);
27
+ manifest.pages[pageName] = pageManifest;
28
+ await writeJson(manifestPath, manifest);
29
+ }
30
+
31
+ export async function readPageManifest(directory: string, pageName: string): Promise<PageAssetManifest> {
32
+ const manifestPath = path.join(directory, MANIFEST_FILENAME);
33
+ const manifest = (await readJson<AssetManifest>(manifestPath)) ?? { pages: {} };
34
+ return manifest.pages[pageName] ?? {};
35
+ }
36
+
37
+ export async function updateSharedAssets(directory: string, updater: (value: SharedAssets) => void): Promise<void> {
38
+ const manifestPath = path.join(directory, MANIFEST_FILENAME);
39
+ await ensureDir(directory);
40
+ const manifest = (await readJson<AssetManifest>(manifestPath)) ?? { pages: {} };
41
+ const shared: SharedAssets = manifest.shared ?? {};
42
+ updater(shared);
43
+ manifest.shared = shared;
44
+ await writeJson(manifestPath, manifest);
45
+ }
46
+
47
+ export async function readSharedAssets(directory: string): Promise<SharedAssets | null> {
48
+ const manifestPath = path.join(directory, MANIFEST_FILENAME);
49
+ const manifest = await readJson<AssetManifest>(manifestPath);
50
+ return manifest?.shared ?? null;
51
+ }