@webstir-io/webstir 0.1.0
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/README.md +69 -0
- package/assets/features/client_nav/client_nav.ts +469 -0
- package/assets/features/content_nav/content_nav.css +170 -0
- package/assets/features/content_nav/content_nav.ts +358 -0
- package/assets/features/router/router-types.ts +6 -0
- package/assets/features/router/router.ts +118 -0
- package/assets/features/search/search.css +204 -0
- package/assets/features/search/search.ts +627 -0
- package/assets/templates/api/src/backend/index.ts +13 -0
- package/assets/templates/api/src/backend/tsconfig.json +15 -0
- package/assets/templates/api/src/shared/router-types.ts +23 -0
- package/assets/templates/api/src/shared/tsconfig.json +10 -0
- package/assets/templates/api/src/shared/types/index.ts +4 -0
- package/assets/templates/full/src/backend/index.ts +13 -0
- package/assets/templates/full/src/backend/tsconfig.json +15 -0
- package/assets/templates/full/src/frontend/app/app.css +65 -0
- package/assets/templates/full/src/frontend/app/app.html +13 -0
- package/assets/templates/full/src/frontend/app/app.ts +188 -0
- package/assets/templates/full/src/frontend/app/error.ts +127 -0
- package/assets/templates/full/src/frontend/app/hmr.js +355 -0
- package/assets/templates/full/src/frontend/app/navigation.ts +8 -0
- package/assets/templates/full/src/frontend/app/refresh.js +114 -0
- package/assets/templates/full/src/frontend/app/router.ts +126 -0
- package/assets/templates/full/src/frontend/app/styles/base.css +2 -0
- package/assets/templates/full/src/frontend/app/styles/reset.css +48 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +21 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +10 -0
- package/assets/templates/full/src/frontend/pages/home/index.ts +18 -0
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +21 -0
- package/assets/templates/full/src/frontend/tsconfig.json +20 -0
- package/assets/templates/full/src/shared/router-types.ts +23 -0
- package/assets/templates/full/src/shared/tsconfig.json +10 -0
- package/assets/templates/full/src/shared/types/index.ts +4 -0
- package/assets/templates/shared/Errors.404.html +23 -0
- package/assets/templates/shared/Errors.500.html +23 -0
- package/assets/templates/shared/Errors.default.html +23 -0
- package/assets/templates/shared/types/global.d.ts +32 -0
- package/assets/templates/shared/types.global.d.ts +32 -0
- package/assets/templates/spa/src/frontend/app/app.css +65 -0
- package/assets/templates/spa/src/frontend/app/app.html +13 -0
- package/assets/templates/spa/src/frontend/app/app.ts +188 -0
- package/assets/templates/spa/src/frontend/app/error.ts +127 -0
- package/assets/templates/spa/src/frontend/app/hmr.js +355 -0
- package/assets/templates/spa/src/frontend/app/navigation.ts +8 -0
- package/assets/templates/spa/src/frontend/app/refresh.js +114 -0
- package/assets/templates/spa/src/frontend/app/router.ts +126 -0
- package/assets/templates/spa/src/frontend/app/styles/base.css +2 -0
- package/assets/templates/spa/src/frontend/app/styles/reset.css +48 -0
- package/assets/templates/spa/src/frontend/pages/home/index.css +21 -0
- package/assets/templates/spa/src/frontend/pages/home/index.html +10 -0
- package/assets/templates/spa/src/frontend/pages/home/index.ts +18 -0
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +21 -0
- package/assets/templates/spa/src/frontend/tsconfig.json +20 -0
- package/assets/templates/spa/src/shared/router-types.ts +23 -0
- package/assets/templates/spa/src/shared/tsconfig.json +10 -0
- package/assets/templates/spa/src/shared/types/index.ts +4 -0
- package/assets/templates/ssg/src/frontend/app/app.css +12 -0
- package/assets/templates/ssg/src/frontend/app/app.html +43 -0
- package/assets/templates/ssg/src/frontend/app/app.ts +190 -0
- package/assets/templates/ssg/src/frontend/app/error.ts +127 -0
- package/assets/templates/ssg/src/frontend/app/hmr.js +370 -0
- package/assets/templates/ssg/src/frontend/app/refresh.js +163 -0
- package/assets/templates/ssg/src/frontend/app/scripts/components/drawer.ts +183 -0
- package/assets/templates/ssg/src/frontend/app/scripts/components/menu.ts +75 -0
- package/assets/templates/ssg/src/frontend/app/styles/base.css +77 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/buttons.css +108 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/drawer.css +12 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/header.css +164 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/markdown.css +25 -0
- package/assets/templates/ssg/src/frontend/app/styles/layout.css +41 -0
- package/assets/templates/ssg/src/frontend/app/styles/reset.css +56 -0
- package/assets/templates/ssg/src/frontend/app/styles/tokens.css +72 -0
- package/assets/templates/ssg/src/frontend/app/styles/utilities.css +14 -0
- package/assets/templates/ssg/src/frontend/content/_sidebar.json +14 -0
- package/assets/templates/ssg/src/frontend/content/content-pipeline.md +82 -0
- package/assets/templates/ssg/src/frontend/content/css-playbook.md +68 -0
- package/assets/templates/ssg/src/frontend/content/hosting.md +48 -0
- package/assets/templates/ssg/src/frontend/pages/about/index.css +33 -0
- package/assets/templates/ssg/src/frontend/pages/about/index.html +60 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.css +505 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.html +52 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.ts +495 -0
- package/assets/templates/ssg/src/frontend/pages/home/index.css +91 -0
- package/assets/templates/ssg/src/frontend/pages/home/index.html +38 -0
- package/assets/templates/ssg/src/frontend/pages/home/tests/home.test.ts +24 -0
- package/assets/templates/ssg/src/frontend/tsconfig.json +13 -0
- package/package.json +41 -0
- package/scripts/pack-standalone.mjs +127 -0
- package/scripts/sync-assets.mjs +87 -0
- package/src/add-backend.ts +164 -0
- package/src/add.ts +112 -0
- package/src/api-watch.ts +84 -0
- package/src/backend-inspect.ts +45 -0
- package/src/backend-runtime.ts +286 -0
- package/src/build-plan.ts +12 -0
- package/src/build.ts +10 -0
- package/src/cli.ts +569 -0
- package/src/compile-tests.ts +61 -0
- package/src/dev-server.ts +393 -0
- package/src/enable-assets.ts +196 -0
- package/src/enable.ts +477 -0
- package/src/execute.ts +85 -0
- package/src/format.ts +254 -0
- package/src/frontend-watch.ts +145 -0
- package/src/full-watch.ts +80 -0
- package/src/index.ts +20 -0
- package/src/init-assets.ts +96 -0
- package/src/init.ts +339 -0
- package/src/paths.ts +26 -0
- package/src/providers.ts +88 -0
- package/src/publish.ts +8 -0
- package/src/refresh.ts +56 -0
- package/src/repair.ts +414 -0
- package/src/runtime.ts +48 -0
- package/src/smoke.ts +161 -0
- package/src/stop-signal.ts +26 -0
- package/src/test.ts +215 -0
- package/src/types.ts +29 -0
- package/src/watch-daemon-client.ts +171 -0
- package/src/watch-events.ts +195 -0
- package/src/watch.ts +66 -0
- package/src/workspace-watcher.ts +251 -0
- package/src/workspace.ts +55 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { runAddPageCommand, runAddTestCommand } from './add.ts';
|
|
8
|
+
import { runAddJobCommand, runAddRouteCommand } from './add-backend.ts';
|
|
9
|
+
import { runBackendInspect } from './backend-inspect.ts';
|
|
10
|
+
import { runEnable } from './enable.ts';
|
|
11
|
+
import {
|
|
12
|
+
formatAddSummary,
|
|
13
|
+
formatBackendInspectSummary,
|
|
14
|
+
formatBuildSummary,
|
|
15
|
+
formatEnableSummary,
|
|
16
|
+
formatInitSummary,
|
|
17
|
+
formatPublishSummary,
|
|
18
|
+
formatRepairSummary,
|
|
19
|
+
formatRefreshSummary,
|
|
20
|
+
formatSmokeSummary,
|
|
21
|
+
formatTestSummary,
|
|
22
|
+
} from './format.ts';
|
|
23
|
+
import { runInit } from './init.ts';
|
|
24
|
+
import { runRepair } from './repair.ts';
|
|
25
|
+
import { runRefresh } from './refresh.ts';
|
|
26
|
+
import { runBuild } from './build.ts';
|
|
27
|
+
import { runPublish } from './publish.ts';
|
|
28
|
+
import { runSmoke } from './smoke.ts';
|
|
29
|
+
import { runTest } from './test.ts';
|
|
30
|
+
import { runWatch } from './watch.ts';
|
|
31
|
+
|
|
32
|
+
interface CliStream {
|
|
33
|
+
write(message: string): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CliIo {
|
|
37
|
+
readonly stdout: CliStream;
|
|
38
|
+
readonly stderr: CliStream;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const HELP_TEXT = `Usage:
|
|
42
|
+
webstir init <mode> <directory>
|
|
43
|
+
webstir init <directory>
|
|
44
|
+
webstir add-page <name> --workspace <path>
|
|
45
|
+
webstir add-test <name-or-path> --workspace <path>
|
|
46
|
+
webstir add-route <name> --workspace <path> [--method <METHOD>] [--path <path>] [--fastify]
|
|
47
|
+
webstir add-job <name> --workspace <path> [--schedule <expression>]
|
|
48
|
+
webstir backend-inspect --workspace <path>
|
|
49
|
+
webstir test --workspace <path> [--runtime <frontend|backend|all>]
|
|
50
|
+
webstir smoke [--workspace <path>]
|
|
51
|
+
webstir build --workspace <path>
|
|
52
|
+
webstir publish --workspace <path>
|
|
53
|
+
webstir enable <feature> [feature-args...] --workspace <path>
|
|
54
|
+
webstir repair --workspace <path> [--dry-run]
|
|
55
|
+
webstir refresh <mode> --workspace <path>
|
|
56
|
+
webstir watch --workspace <path> [--host <host>] [--port <port>]
|
|
57
|
+
|
|
58
|
+
Commands:
|
|
59
|
+
init Scaffold a new Webstir workspace.
|
|
60
|
+
add-page Scaffold a frontend page in an existing workspace.
|
|
61
|
+
add-test Scaffold a test file in an existing workspace.
|
|
62
|
+
add-route Scaffold a backend route in an existing workspace.
|
|
63
|
+
add-job Scaffold a backend job in an existing workspace.
|
|
64
|
+
backend-inspect Inspect the backend manifest for an existing workspace.
|
|
65
|
+
test Build and run workspace tests with the Bun orchestrator.
|
|
66
|
+
smoke Run an end-to-end Bun orchestrator verification flow.
|
|
67
|
+
build Build a Webstir workspace with the Bun orchestrator.
|
|
68
|
+
publish Publish a Webstir workspace with the Bun orchestrator.
|
|
69
|
+
enable Scaffold an optional Webstir feature into a workspace.
|
|
70
|
+
repair Restore missing scaffold-managed workspace files.
|
|
71
|
+
refresh Reset and re-scaffold an existing workspace directory.
|
|
72
|
+
watch Run the Bun dev loop for a supported Webstir workspace.
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
-w, --workspace <path> Workspace root to operate on.
|
|
76
|
+
--host <host> Dev host or bind address (default: 127.0.0.1).
|
|
77
|
+
--port <port> Dev port (SPA default: 8088, API default: 4321).
|
|
78
|
+
--dry-run Report repair changes without writing files.
|
|
79
|
+
-v, --verbose Enable verbose frontend watch diagnostics.
|
|
80
|
+
--hmr-verbose Enable detailed hot-update diagnostics.
|
|
81
|
+
-h, --help Show this help text.
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Promise<number> {
|
|
85
|
+
if (argv.length === 0 || argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h') {
|
|
86
|
+
io.stdout.write(HELP_TEXT);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const [command, ...rest] = argv;
|
|
91
|
+
if (
|
|
92
|
+
command !== 'init'
|
|
93
|
+
&& command !== 'add-page'
|
|
94
|
+
&& command !== 'add-test'
|
|
95
|
+
&& command !== 'add-route'
|
|
96
|
+
&& command !== 'add-job'
|
|
97
|
+
&& command !== 'backend-inspect'
|
|
98
|
+
&& command !== 'test'
|
|
99
|
+
&& command !== 'smoke'
|
|
100
|
+
&& command !== 'build'
|
|
101
|
+
&& command !== 'publish'
|
|
102
|
+
&& command !== 'enable'
|
|
103
|
+
&& command !== 'repair'
|
|
104
|
+
&& command !== 'refresh'
|
|
105
|
+
&& command !== 'watch'
|
|
106
|
+
) {
|
|
107
|
+
io.stderr.write(`Unknown command "${command}".\n\n${HELP_TEXT}`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const options = parseCommandOptions(rest, {
|
|
112
|
+
allowUnknownOptions: command === 'add-route' || command === 'add-job',
|
|
113
|
+
});
|
|
114
|
+
if (options.help) {
|
|
115
|
+
io.stdout.write(HELP_TEXT);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (options.error) {
|
|
120
|
+
io.stderr.write(`${options.error}\n\n${HELP_TEXT}`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (command !== 'repair' && options.dryRun) {
|
|
125
|
+
io.stderr.write(`Only repair accepts --dry-run.\n\n${HELP_TEXT}`);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const workspaceRoot = options.workspaceRoot;
|
|
130
|
+
if (command !== 'init' && command !== 'smoke' && !workspaceRoot) {
|
|
131
|
+
io.stderr.write(`Missing required --workspace <path>.\n\n${HELP_TEXT}`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (command === 'init') {
|
|
137
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
138
|
+
io.stderr.write(`Init does not accept watch options.\n\n${HELP_TEXT}`);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = await runInit({
|
|
143
|
+
args: options.positionals,
|
|
144
|
+
workspaceRoot,
|
|
145
|
+
});
|
|
146
|
+
io.stdout.write(`${formatInitSummary(result)}\n`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const resolvedWorkspaceRoot = workspaceRoot ? path.resolve(workspaceRoot) : undefined;
|
|
151
|
+
if (command === 'add-page') {
|
|
152
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
153
|
+
io.stderr.write(`Add-page does not accept watch options.\n\n${HELP_TEXT}`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result = await runAddPageCommand({
|
|
158
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
159
|
+
args: options.positionals,
|
|
160
|
+
});
|
|
161
|
+
io.stdout.write(
|
|
162
|
+
`${formatAddSummary('[webstir] add-page complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
|
|
163
|
+
);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (command === 'add-test') {
|
|
168
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
169
|
+
io.stderr.write(`Add-test does not accept watch options.\n\n${HELP_TEXT}`);
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await runAddTestCommand({
|
|
174
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
175
|
+
args: options.positionals,
|
|
176
|
+
});
|
|
177
|
+
io.stdout.write(
|
|
178
|
+
`${formatAddSummary('[webstir] add-test complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
|
|
179
|
+
);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (command === 'add-route') {
|
|
184
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
185
|
+
io.stderr.write(`Add-route does not accept watch options.\n\n${HELP_TEXT}`);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await runAddRouteCommand({
|
|
190
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
191
|
+
rawArgs: options.rawArgs,
|
|
192
|
+
});
|
|
193
|
+
io.stdout.write(
|
|
194
|
+
`${formatAddSummary('[webstir] add-route complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
|
|
195
|
+
);
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (command === 'add-job') {
|
|
200
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
201
|
+
io.stderr.write(`Add-job does not accept watch options.\n\n${HELP_TEXT}`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await runAddJobCommand({
|
|
206
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
207
|
+
rawArgs: options.rawArgs,
|
|
208
|
+
});
|
|
209
|
+
io.stdout.write(
|
|
210
|
+
`${formatAddSummary('[webstir] add-job complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
|
|
211
|
+
);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (command === 'backend-inspect') {
|
|
216
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
217
|
+
io.stderr.write(`Backend-inspect does not accept watch options.\n\n${HELP_TEXT}`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (options.positionals.length > 0) {
|
|
222
|
+
io.stderr.write(`Backend-inspect does not accept positional arguments.\n\n${HELP_TEXT}`);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = await runBackendInspect({
|
|
227
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
228
|
+
});
|
|
229
|
+
io.stdout.write(`${formatBackendInspectSummary(result)}\n`);
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (command === 'test') {
|
|
234
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
235
|
+
io.stderr.write(`Test does not accept watch options.\n\n${HELP_TEXT}`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = await runTest({
|
|
240
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
241
|
+
rawArgs: options.rawArgs,
|
|
242
|
+
});
|
|
243
|
+
io.stdout.write(`${formatTestSummary(result)}\n`);
|
|
244
|
+
return result.hadFailures ? 1 : 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (command === 'smoke') {
|
|
248
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
249
|
+
io.stderr.write(`Smoke does not accept watch options.\n\n${HELP_TEXT}`);
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (options.positionals.length > 0) {
|
|
254
|
+
io.stderr.write(`Smoke does not accept positional arguments.\n\n${HELP_TEXT}`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const result = await runSmoke({
|
|
259
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
260
|
+
});
|
|
261
|
+
io.stdout.write(`${formatSmokeSummary(result)}\n`);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (command === 'build') {
|
|
266
|
+
if (options.positionals.length > 0) {
|
|
267
|
+
io.stderr.write(`Build does not accept positional arguments.\n\n${HELP_TEXT}`);
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const result = await runBuild({
|
|
272
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
273
|
+
});
|
|
274
|
+
io.stdout.write(`${formatBuildSummary(result)}\n`);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (command === 'publish') {
|
|
279
|
+
if (options.positionals.length > 0) {
|
|
280
|
+
io.stderr.write(`Publish does not accept positional arguments.\n\n${HELP_TEXT}`);
|
|
281
|
+
return 1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const result = await runPublish({
|
|
285
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
286
|
+
});
|
|
287
|
+
io.stdout.write(`${formatPublishSummary(result)}\n`);
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (command === 'enable') {
|
|
292
|
+
const result = await runEnable({
|
|
293
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
294
|
+
args: options.positionals,
|
|
295
|
+
});
|
|
296
|
+
io.stdout.write(`${formatEnableSummary(result)}\n`);
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (command === 'repair') {
|
|
301
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
302
|
+
io.stderr.write(`Repair does not accept watch options.\n\n${HELP_TEXT}`);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (options.positionals.length > 0) {
|
|
307
|
+
io.stderr.write(`Repair does not accept positional arguments.\n\n${HELP_TEXT}`);
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = await runRepair({
|
|
312
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
313
|
+
rawArgs: options.rawArgs,
|
|
314
|
+
});
|
|
315
|
+
io.stdout.write(`${formatRepairSummary(result)}\n`);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (command === 'refresh') {
|
|
320
|
+
if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
|
|
321
|
+
io.stderr.write(`Refresh does not accept watch options.\n\n${HELP_TEXT}`);
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const result = await runRefresh({
|
|
326
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
327
|
+
args: options.positionals,
|
|
328
|
+
});
|
|
329
|
+
io.stdout.write(`${formatRefreshSummary(result)}\n`);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (options.positionals.length > 0) {
|
|
334
|
+
io.stderr.write(`Watch does not accept positional arguments.\n\n${HELP_TEXT}`);
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
await runWatch({
|
|
339
|
+
workspaceRoot: resolvedWorkspaceRoot!,
|
|
340
|
+
host: options.host,
|
|
341
|
+
port: options.port,
|
|
342
|
+
verbose: options.verbose,
|
|
343
|
+
hmrVerbose: options.hmrVerbose,
|
|
344
|
+
io,
|
|
345
|
+
});
|
|
346
|
+
return 0;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
349
|
+
io.stderr.write(`[webstir] ${command} failed: ${message}\n`);
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
interface ParsedCommandOptions {
|
|
355
|
+
readonly workspaceRoot?: string;
|
|
356
|
+
readonly host?: string;
|
|
357
|
+
readonly port?: number;
|
|
358
|
+
readonly dryRun: boolean;
|
|
359
|
+
readonly verbose: boolean;
|
|
360
|
+
readonly hmrVerbose: boolean;
|
|
361
|
+
readonly positionals: readonly string[];
|
|
362
|
+
readonly rawArgs: readonly string[];
|
|
363
|
+
readonly help: boolean;
|
|
364
|
+
readonly error?: string;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function parseCommandOptions(
|
|
368
|
+
args: readonly string[],
|
|
369
|
+
options: { readonly allowUnknownOptions?: boolean } = {}
|
|
370
|
+
): ParsedCommandOptions {
|
|
371
|
+
let workspaceRoot: string | undefined;
|
|
372
|
+
let host: string | undefined;
|
|
373
|
+
let port: number | undefined;
|
|
374
|
+
let dryRun = false;
|
|
375
|
+
let verbose = false;
|
|
376
|
+
let hmrVerbose = false;
|
|
377
|
+
const positionals: string[] = [];
|
|
378
|
+
|
|
379
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
380
|
+
const arg = args[index];
|
|
381
|
+
if (!arg.startsWith('-')) {
|
|
382
|
+
positionals.push(arg);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (arg === '--workspace' || arg === '-w') {
|
|
387
|
+
const next = args[index + 1];
|
|
388
|
+
if (!next || next.startsWith('-')) {
|
|
389
|
+
return {
|
|
390
|
+
workspaceRoot,
|
|
391
|
+
host,
|
|
392
|
+
port,
|
|
393
|
+
dryRun,
|
|
394
|
+
verbose,
|
|
395
|
+
hmrVerbose,
|
|
396
|
+
positionals,
|
|
397
|
+
rawArgs: args,
|
|
398
|
+
help: false,
|
|
399
|
+
error: 'Missing value for --workspace.',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
workspaceRoot = next;
|
|
404
|
+
index += 1;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (arg === '--host') {
|
|
409
|
+
const next = args[index + 1];
|
|
410
|
+
if (!next || next.startsWith('-')) {
|
|
411
|
+
return {
|
|
412
|
+
workspaceRoot,
|
|
413
|
+
host,
|
|
414
|
+
port,
|
|
415
|
+
dryRun,
|
|
416
|
+
verbose,
|
|
417
|
+
hmrVerbose,
|
|
418
|
+
positionals,
|
|
419
|
+
rawArgs: args,
|
|
420
|
+
help: false,
|
|
421
|
+
error: 'Missing value for --host.',
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
host = next;
|
|
426
|
+
index += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (arg === '--port') {
|
|
431
|
+
const rawPort = args[index + 1];
|
|
432
|
+
const parsedPort = Number.parseInt(rawPort ?? '', 10);
|
|
433
|
+
if (!Number.isFinite(parsedPort) || parsedPort < 0) {
|
|
434
|
+
return {
|
|
435
|
+
workspaceRoot,
|
|
436
|
+
host,
|
|
437
|
+
port,
|
|
438
|
+
dryRun,
|
|
439
|
+
verbose,
|
|
440
|
+
hmrVerbose,
|
|
441
|
+
positionals,
|
|
442
|
+
rawArgs: args,
|
|
443
|
+
help: false,
|
|
444
|
+
error: `Invalid --port value "${rawPort ?? ''}".`,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
port = parsedPort;
|
|
449
|
+
index += 1;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (arg === '--runtime' || arg === '-r') {
|
|
454
|
+
const next = args[index + 1];
|
|
455
|
+
if (!next || next.startsWith('-')) {
|
|
456
|
+
return {
|
|
457
|
+
workspaceRoot,
|
|
458
|
+
host,
|
|
459
|
+
port,
|
|
460
|
+
dryRun,
|
|
461
|
+
verbose,
|
|
462
|
+
hmrVerbose,
|
|
463
|
+
positionals,
|
|
464
|
+
rawArgs: args,
|
|
465
|
+
help: false,
|
|
466
|
+
error: 'Missing value for --runtime.',
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
index += 1;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (arg.startsWith('--runtime=')) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (arg === '--verbose' || arg === '-v') {
|
|
479
|
+
verbose = true;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (arg === '--hmr-verbose') {
|
|
484
|
+
hmrVerbose = true;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (arg === '--help' || arg === '-h') {
|
|
489
|
+
return {
|
|
490
|
+
workspaceRoot,
|
|
491
|
+
host,
|
|
492
|
+
port,
|
|
493
|
+
dryRun,
|
|
494
|
+
verbose,
|
|
495
|
+
hmrVerbose,
|
|
496
|
+
positionals,
|
|
497
|
+
rawArgs: args,
|
|
498
|
+
help: true,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (arg === '--dry-run') {
|
|
503
|
+
dryRun = true;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (options.allowUnknownOptions) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
workspaceRoot,
|
|
513
|
+
host,
|
|
514
|
+
port,
|
|
515
|
+
dryRun,
|
|
516
|
+
verbose,
|
|
517
|
+
hmrVerbose,
|
|
518
|
+
positionals,
|
|
519
|
+
rawArgs: args,
|
|
520
|
+
help: false,
|
|
521
|
+
error: `Unknown option "${arg}".`,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
workspaceRoot,
|
|
527
|
+
host,
|
|
528
|
+
port,
|
|
529
|
+
dryRun,
|
|
530
|
+
verbose,
|
|
531
|
+
hmrVerbose,
|
|
532
|
+
positionals,
|
|
533
|
+
rawArgs: args,
|
|
534
|
+
help: false,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const defaultIo: CliIo = {
|
|
539
|
+
stdout: {
|
|
540
|
+
write(message) {
|
|
541
|
+
process.stdout.write(message);
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
stderr: {
|
|
545
|
+
write(message) {
|
|
546
|
+
process.stderr.write(message);
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
async function main(): Promise<void> {
|
|
552
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
553
|
+
if (exitCode !== 0) {
|
|
554
|
+
process.exitCode = exitCode;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
559
|
+
if (process.argv[1] && resolveRealpath(process.argv[1]) === resolveRealpath(currentFile)) {
|
|
560
|
+
await main();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function resolveRealpath(filePath: string): string {
|
|
564
|
+
try {
|
|
565
|
+
return realpathSync(filePath);
|
|
566
|
+
} catch {
|
|
567
|
+
return path.resolve(filePath);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
|
|
6
|
+
import type { TestModule } from '@webstir-io/webstir-testing';
|
|
7
|
+
|
|
8
|
+
const TESTING_PACKAGE_SPECIFIER = '@webstir-io/webstir-testing';
|
|
9
|
+
const TESTING_RUNTIME_SPECIFIER = import.meta.resolve(TESTING_PACKAGE_SPECIFIER);
|
|
10
|
+
|
|
11
|
+
export async function compileTestModules(workspaceRoot: string, modules: readonly TestModule[]): Promise<void> {
|
|
12
|
+
const shimPath = await ensureTestingRuntimeShim(workspaceRoot);
|
|
13
|
+
|
|
14
|
+
await Promise.all(modules.map(async (module) => {
|
|
15
|
+
if (!module.compiledPath) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await mkdir(path.dirname(module.compiledPath), { recursive: true });
|
|
20
|
+
|
|
21
|
+
if (module.sourcePath.endsWith('.js')) {
|
|
22
|
+
const source = await readFile(module.sourcePath, 'utf8');
|
|
23
|
+
const rewritten = rewriteTestingImports(source, module.compiledPath, shimPath);
|
|
24
|
+
await writeFile(module.compiledPath, rewritten, 'utf8');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const source = await readFile(module.sourcePath, 'utf8');
|
|
29
|
+
const output = ts.transpileModule(source, {
|
|
30
|
+
fileName: module.sourcePath,
|
|
31
|
+
compilerOptions: {
|
|
32
|
+
target: ts.ScriptTarget.ES2022,
|
|
33
|
+
module: ts.ModuleKind.ESNext,
|
|
34
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
35
|
+
verbatimModuleSyntax: true,
|
|
36
|
+
rewriteRelativeImportExtensions: true,
|
|
37
|
+
},
|
|
38
|
+
reportDiagnostics: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const rewritten = rewriteTestingImports(output.outputText, module.compiledPath, shimPath);
|
|
42
|
+
await writeFile(module.compiledPath, rewritten, 'utf8');
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function ensureTestingRuntimeShim(workspaceRoot: string): Promise<string> {
|
|
47
|
+
const shimPath = path.join(workspaceRoot, 'build', '.webstir', 'testing-runtime.mjs');
|
|
48
|
+
const contents = `export { test, assert } from ${JSON.stringify(TESTING_RUNTIME_SPECIFIER)};\n`;
|
|
49
|
+
await mkdir(path.dirname(shimPath), { recursive: true });
|
|
50
|
+
await writeFile(shimPath, contents, 'utf8');
|
|
51
|
+
return shimPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rewriteTestingImports(source: string, compiledPath: string, shimPath: string): string {
|
|
55
|
+
const relativeShim = path.relative(path.dirname(compiledPath), shimPath).split(path.sep).join('/');
|
|
56
|
+
const specifier = relativeShim.startsWith('.') ? relativeShim : `./${relativeShim}`;
|
|
57
|
+
|
|
58
|
+
return source
|
|
59
|
+
.replaceAll(`'${TESTING_PACKAGE_SPECIFIER}'`, `'${specifier}'`)
|
|
60
|
+
.replaceAll(`"${TESTING_PACKAGE_SPECIFIER}"`, `"${specifier}"`);
|
|
61
|
+
}
|