@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.
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/assets/assetManifest.d.ts +16 -0
- package/dist/assets/assetManifest.js +31 -0
- package/dist/assets/imageOptimizer.d.ts +6 -0
- package/dist/assets/imageOptimizer.js +93 -0
- package/dist/assets/precompression.d.ts +1 -0
- package/dist/assets/precompression.js +21 -0
- package/dist/builders/contentBuilder.d.ts +2 -0
- package/dist/builders/contentBuilder.js +1052 -0
- package/dist/builders/cssBuilder.d.ts +2 -0
- package/dist/builders/cssBuilder.js +439 -0
- package/dist/builders/htmlBuilder.d.ts +2 -0
- package/dist/builders/htmlBuilder.js +430 -0
- package/dist/builders/index.d.ts +2 -0
- package/dist/builders/index.js +14 -0
- package/dist/builders/jsBuilder.d.ts +2 -0
- package/dist/builders/jsBuilder.js +300 -0
- package/dist/builders/staticAssetsBuilder.d.ts +2 -0
- package/dist/builders/staticAssetsBuilder.js +158 -0
- package/dist/builders/types.d.ts +12 -0
- package/dist/builders/types.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +105 -0
- package/dist/config/manifest.d.ts +7 -0
- package/dist/config/manifest.js +17 -0
- package/dist/config/paths.d.ts +3 -0
- package/dist/config/paths.js +11 -0
- package/dist/config/schema.d.ts +413 -0
- package/dist/config/schema.js +44 -0
- package/dist/config/setup.d.ts +2 -0
- package/dist/config/setup.js +12 -0
- package/dist/config/workspace.d.ts +2 -0
- package/dist/config/workspace.js +131 -0
- package/dist/config/workspaceManifest.d.ts +23 -0
- package/dist/config/workspaceManifest.js +1 -0
- package/dist/core/constants.d.ts +70 -0
- package/dist/core/constants.js +70 -0
- package/dist/core/diagnostics.d.ts +15 -0
- package/dist/core/diagnostics.js +21 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/dist/core/pages.d.ts +6 -0
- package/dist/core/pages.js +23 -0
- package/dist/hooks.d.ts +19 -0
- package/dist/hooks.js +115 -0
- package/dist/html/criticalCss.d.ts +4 -0
- package/dist/html/criticalCss.js +192 -0
- package/dist/html/htmlSecurity.d.ts +5 -0
- package/dist/html/htmlSecurity.js +73 -0
- package/dist/html/lazyLoad.d.ts +6 -0
- package/dist/html/lazyLoad.js +21 -0
- package/dist/html/pageScaffold.d.ts +10 -0
- package/dist/html/pageScaffold.js +51 -0
- package/dist/html/resourceHints.d.ts +7 -0
- package/dist/html/resourceHints.js +64 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/modes/ssg/index.d.ts +4 -0
- package/dist/modes/ssg/index.js +4 -0
- package/dist/modes/ssg/metadata.d.ts +5 -0
- package/dist/modes/ssg/metadata.js +50 -0
- package/dist/modes/ssg/routing.d.ts +2 -0
- package/dist/modes/ssg/routing.js +186 -0
- package/dist/modes/ssg/seo.d.ts +4 -0
- package/dist/modes/ssg/seo.js +208 -0
- package/dist/modes/ssg/validation.d.ts +3 -0
- package/dist/modes/ssg/validation.js +27 -0
- package/dist/modes/ssg/views.d.ts +2 -0
- package/dist/modes/ssg/views.js +236 -0
- package/dist/operations.d.ts +5 -0
- package/dist/operations.js +102 -0
- package/dist/pipeline.d.ts +7 -0
- package/dist/pipeline.js +71 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +176 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/dist/utils/changedFile.d.ts +8 -0
- package/dist/utils/changedFile.js +26 -0
- package/dist/utils/fs.d.ts +11 -0
- package/dist/utils/fs.js +39 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +5 -0
- package/dist/utils/pagePaths.d.ts +5 -0
- package/dist/utils/pagePaths.js +36 -0
- package/dist/utils/pathMatch.d.ts +3 -0
- package/dist/utils/pathMatch.js +29 -0
- package/dist/watch/frontendFiles.d.ts +3 -0
- package/dist/watch/frontendFiles.js +25 -0
- package/dist/watch/hotUpdateTracker.d.ts +51 -0
- package/dist/watch/hotUpdateTracker.js +205 -0
- package/dist/watch/pipelineHelpers.d.ts +26 -0
- package/dist/watch/pipelineHelpers.js +177 -0
- package/dist/watch/types.d.ts +27 -0
- package/dist/watch/types.js +1 -0
- package/dist/watch/watchCoordinator.d.ts +36 -0
- package/dist/watch/watchCoordinator.js +551 -0
- package/dist/watch/watchDaemon.d.ts +17 -0
- package/dist/watch/watchDaemon.js +127 -0
- package/dist/watch/watchReporter.d.ts +21 -0
- package/dist/watch/watchReporter.js +64 -0
- package/package.json +92 -0
- package/scripts/publish.sh +101 -0
- package/scripts/smoke.mjs +35 -0
- package/scripts/update-contract.sh +121 -0
- package/src/assets/assetManifest.ts +51 -0
- package/src/assets/imageOptimizer.ts +112 -0
- package/src/assets/precompression.ts +25 -0
- package/src/builders/contentBuilder.ts +1400 -0
- package/src/builders/cssBuilder.ts +552 -0
- package/src/builders/htmlBuilder.ts +540 -0
- package/src/builders/index.ts +16 -0
- package/src/builders/jsBuilder.ts +358 -0
- package/src/builders/staticAssetsBuilder.ts +174 -0
- package/src/builders/types.ts +15 -0
- package/src/cli.ts +108 -0
- package/src/config/manifest.ts +24 -0
- package/src/config/paths.ts +14 -0
- package/src/config/schema.ts +49 -0
- package/src/config/setup.ts +14 -0
- package/src/config/workspace.ts +150 -0
- package/src/config/workspaceManifest.ts +27 -0
- package/src/core/constants.ts +73 -0
- package/src/core/diagnostics.ts +40 -0
- package/src/core/index.ts +3 -0
- package/src/core/pages.ts +31 -0
- package/src/hooks.ts +175 -0
- package/src/html/criticalCss.ts +214 -0
- package/src/html/htmlSecurity.ts +86 -0
- package/src/html/lazyLoad.ts +30 -0
- package/src/html/pageScaffold.ts +70 -0
- package/src/html/resourceHints.ts +91 -0
- package/src/index.ts +5 -0
- package/src/modes/ssg/index.ts +4 -0
- package/src/modes/ssg/metadata.ts +63 -0
- package/src/modes/ssg/routing.ts +230 -0
- package/src/modes/ssg/seo.ts +261 -0
- package/src/modes/ssg/validation.ts +37 -0
- package/src/modes/ssg/views.ts +309 -0
- package/src/operations.ts +138 -0
- package/src/pipeline.ts +88 -0
- package/src/provider.ts +249 -0
- package/src/types.ts +67 -0
- package/src/utils/changedFile.ts +39 -0
- package/src/utils/fs.ts +48 -0
- package/src/utils/hash.ts +6 -0
- package/src/utils/pagePaths.ts +43 -0
- package/src/utils/pathMatch.ts +36 -0
- package/src/watch/frontendFiles.ts +32 -0
- package/src/watch/hotUpdateTracker.ts +285 -0
- package/src/watch/pipelineHelpers.ts +242 -0
- package/src/watch/types.ts +23 -0
- package/src/watch/watchCoordinator.ts +666 -0
- package/src/watch/watchDaemon.ts +144 -0
- package/src/watch/watchReporter.ts +98 -0
- package/tests/add-page-defaults.test.js +64 -0
- package/tests/content-pages.test.js +81 -0
- package/tests/css-app-imports.test.js +64 -0
- package/tests/css-page-imports.test.js +100 -0
- package/tests/diagnostics.test.js +48 -0
- package/tests/features.test.js +63 -0
- package/tests/hooks.test.js +71 -0
- package/tests/provider.integration.test.js +137 -0
- package/tests/ssg-defaults.test.js +201 -0
- package/tests/ssg-guardrails.test.js +69 -0
- 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
|
+
}
|