@webstir-io/webstir 0.1.1 → 0.1.2
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 +13 -0
- package/assets/deployment/docker/.dockerignore +7 -0
- package/assets/deployment/docker/Dockerfile +17 -0
- package/assets/deployment/docker/README.md +44 -0
- package/assets/deployment/docker/example.env +3 -0
- package/assets/features/client_nav/client_nav.ts +369 -264
- package/assets/features/client_nav/document_navigation.ts +344 -0
- package/assets/features/client_nav/form_enhancement.ts +275 -0
- package/assets/templates/api/src/backend/index.ts +71 -10
- package/assets/templates/api/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/backend/index.ts +71 -10
- package/assets/templates/full/src/backend/module.ts +515 -0
- package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
- package/assets/templates/full/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
- package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
- package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
- package/package.json +31 -13
- package/scripts/check-feature-projections.mjs +87 -0
- package/scripts/check-full-demo-sync.mjs +89 -0
- package/scripts/check-package-install.mjs +537 -0
- package/scripts/check-standalone-install.mjs +221 -0
- package/scripts/pack-standalone.mjs +52 -28
- package/scripts/publish.sh +9 -0
- package/scripts/run-tests.mjs +99 -0
- package/scripts/sync-assets.mjs +175 -17
- package/src/add-backend-compat.ts +628 -0
- package/src/add-backend.ts +155 -27
- package/src/add.ts +111 -4
- package/src/agent.ts +393 -0
- package/src/api-watch.ts +7 -4
- package/src/backend-inspect.ts +70 -2
- package/src/backend-runtime.ts +22 -14
- package/src/build.ts +1 -3
- package/src/bun-generated-frontend-watch.ts +209 -0
- package/src/bun-globals.d.ts +23 -0
- package/src/bun-spa-document.ts +310 -0
- package/src/bun-spa-routes.ts +159 -0
- package/src/bun-spa-watch.ts +29 -0
- package/src/bun-ssg-watch.ts +304 -0
- package/src/cli.ts +381 -50
- package/src/compile-tests.ts +37 -29
- package/src/dev-server.ts +214 -143
- package/src/doctor.ts +164 -0
- package/src/enable-assets.ts +18 -1
- package/src/enable.ts +133 -41
- package/src/execute.ts +28 -4
- package/src/external-workspace.ts +178 -0
- package/src/format.ts +296 -17
- package/src/frontend-inspect.ts +32 -0
- package/src/frontend-watch.ts +27 -102
- package/src/full-watch.ts +13 -18
- package/src/index.ts +7 -0
- package/src/init-assets.ts +41 -11
- package/src/init.ts +85 -71
- package/src/inspect.ts +112 -0
- package/src/mcp/run-cli-json.ts +46 -0
- package/src/mcp/server.ts +307 -0
- package/src/operations.ts +176 -0
- package/src/providers.ts +20 -18
- package/src/refresh.ts +29 -3
- package/src/repair.ts +110 -43
- package/src/runtime-filter.ts +41 -0
- package/src/runtime.ts +1 -1
- package/src/smoke.ts +48 -16
- package/src/test.ts +54 -16
- package/src/testing-runtime.ts +273 -0
- package/src/types.ts +1 -4
- package/src/watch-events.ts +46 -17
- package/src/watch.ts +5 -1
- package/src/workspace-watcher.ts +10 -6
- package/src/workspace.ts +4 -2
- package/src/watch-daemon-client.ts +0 -171
package/src/refresh.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { mkdir, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, readdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
|
|
4
5
|
import { scaffoldWorkspace } from './init.ts';
|
|
5
6
|
import type { WorkspaceMode } from './types.ts';
|
|
@@ -24,10 +25,11 @@ export async function runRefresh(options: RunRefreshOptions): Promise<RefreshRes
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const mode = parseWorkspaceMode(modeToken);
|
|
28
|
+
const metadata = await readWorkspaceManifestMetadata(workspaceRoot);
|
|
27
29
|
await mkdir(workspaceRoot, { recursive: true });
|
|
28
30
|
await emptyDirectory(workspaceRoot);
|
|
29
31
|
|
|
30
|
-
const result = await scaffoldWorkspace(mode, workspaceRoot, { force: true });
|
|
32
|
+
const result = await scaffoldWorkspace(mode, workspaceRoot, { force: true, metadata });
|
|
31
33
|
return {
|
|
32
34
|
workspaceRoot: result.workspaceRoot,
|
|
33
35
|
mode: result.mode,
|
|
@@ -44,7 +46,12 @@ async function emptyDirectory(directoryPath: string): Promise<void> {
|
|
|
44
46
|
|
|
45
47
|
function parseWorkspaceMode(value: string): WorkspaceMode {
|
|
46
48
|
const normalized = value.trim().toLowerCase();
|
|
47
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
normalized === 'ssg' ||
|
|
51
|
+
normalized === 'spa' ||
|
|
52
|
+
normalized === 'api' ||
|
|
53
|
+
normalized === 'full'
|
|
54
|
+
) {
|
|
48
55
|
return normalized;
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -54,3 +61,22 @@ function parseWorkspaceMode(value: string): WorkspaceMode {
|
|
|
54
61
|
|
|
55
62
|
throw new Error(`Unknown refresh mode "${value}". Expected ssg, spa, api, or full.`);
|
|
56
63
|
}
|
|
64
|
+
|
|
65
|
+
async function readWorkspaceManifestMetadata(
|
|
66
|
+
workspaceRoot: string,
|
|
67
|
+
): Promise<{ readonly packageName?: string; readonly description?: string } | undefined> {
|
|
68
|
+
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
69
|
+
if (!existsSync(packageJsonPath)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as {
|
|
74
|
+
readonly name?: string;
|
|
75
|
+
readonly description?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
packageName: packageJson.name,
|
|
80
|
+
description: packageJson.description,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/repair.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { chmod,
|
|
2
|
+
import { chmod, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
|
|
5
5
|
import { getBackendScaffoldAssets } from '@webstir-io/webstir-backend';
|
|
@@ -47,12 +47,17 @@ export async function runRepair(options: RunRepairOptions): Promise<RepairResult
|
|
|
47
47
|
const dryRun = options.rawArgs.includes('--dry-run');
|
|
48
48
|
const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
|
|
49
49
|
const packageJsonPath = path.join(workspace.root, 'package.json');
|
|
50
|
-
const packageJson = JSON.parse(await
|
|
50
|
+
const packageJson = JSON.parse(await readTextFile(packageJsonPath)) as RepairPackageJson;
|
|
51
51
|
const enable = packageJson.webstir?.enable ?? {};
|
|
52
52
|
const changes: string[] = [];
|
|
53
53
|
|
|
54
54
|
await restoreScaffoldAssets(workspace.root, getRootScaffoldAssets(), changes, dryRun);
|
|
55
|
-
await restoreScaffoldAssets(
|
|
55
|
+
await restoreScaffoldAssets(
|
|
56
|
+
workspace.root,
|
|
57
|
+
filterModeScaffoldAssets(await getModeScaffoldAssets(workspace.mode), enable),
|
|
58
|
+
changes,
|
|
59
|
+
dryRun,
|
|
60
|
+
);
|
|
56
61
|
|
|
57
62
|
if (enable.spa) {
|
|
58
63
|
await restoreFeatureAssets(workspace.root, getSpaAssets(), changes, dryRun);
|
|
@@ -64,7 +69,13 @@ export async function runRepair(options: RunRepairOptions): Promise<RepairResult
|
|
|
64
69
|
if (enable.search) {
|
|
65
70
|
await restoreFeatureAssets(workspace.root, getSearchAssets(), changes, dryRun);
|
|
66
71
|
await ensureCssLayerIncludes(workspace.root, 'features', changes, dryRun);
|
|
67
|
-
await ensureAppCssImport(
|
|
72
|
+
await ensureAppCssImport(
|
|
73
|
+
workspace.root,
|
|
74
|
+
'./styles/features/search.css',
|
|
75
|
+
'./styles/components/buttons.css',
|
|
76
|
+
changes,
|
|
77
|
+
dryRun,
|
|
78
|
+
);
|
|
68
79
|
await ensureAppImport(workspace.root, './scripts/features/search.js', changes, dryRun);
|
|
69
80
|
await ensureHtmlSearchMode(workspace.root, changes, dryRun);
|
|
70
81
|
}
|
|
@@ -76,12 +87,14 @@ export async function runRepair(options: RunRepairOptions): Promise<RepairResult
|
|
|
76
87
|
'./styles/features/content-nav.css',
|
|
77
88
|
'./styles/components/buttons.css',
|
|
78
89
|
changes,
|
|
79
|
-
dryRun
|
|
90
|
+
dryRun,
|
|
80
91
|
);
|
|
81
92
|
await ensureAppImport(workspace.root, './scripts/features/content-nav.js', changes, dryRun);
|
|
82
93
|
}
|
|
83
|
-
if (enable.backend
|
|
94
|
+
if (enable.backend) {
|
|
84
95
|
await restoreBackendAssets(workspace.root, changes, dryRun);
|
|
96
|
+
}
|
|
97
|
+
if (enable.backend || workspace.mode === 'api' || workspace.mode === 'full') {
|
|
85
98
|
await ensureBackendTsReference(workspace.root, changes, dryRun);
|
|
86
99
|
}
|
|
87
100
|
if (enable.githubPages) {
|
|
@@ -98,11 +111,24 @@ export async function runRepair(options: RunRepairOptions): Promise<RepairResult
|
|
|
98
111
|
};
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
function filterModeScaffoldAssets(
|
|
115
|
+
assets: readonly { sourcePath: string; targetPath: string }[],
|
|
116
|
+
enable: RepairEnableFlags,
|
|
117
|
+
): readonly { sourcePath: string; targetPath: string }[] {
|
|
118
|
+
if (!enable.backend) {
|
|
119
|
+
return assets;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return assets.filter(
|
|
123
|
+
(asset) => !normalizeRelativePath(asset.targetPath).startsWith('src/backend/'),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
async function restoreScaffoldAssets(
|
|
102
128
|
workspaceRoot: string,
|
|
103
129
|
assets: readonly { sourcePath: string; targetPath: string }[],
|
|
104
130
|
changes: string[],
|
|
105
|
-
dryRun: boolean
|
|
131
|
+
dryRun: boolean,
|
|
106
132
|
): Promise<void> {
|
|
107
133
|
for (const asset of assets) {
|
|
108
134
|
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
@@ -112,7 +138,7 @@ async function restoreScaffoldAssets(
|
|
|
112
138
|
|
|
113
139
|
if (!dryRun) {
|
|
114
140
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
115
|
-
await
|
|
141
|
+
await Bun.write(targetPath, Bun.file(asset.sourcePath));
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
changes.push(relativeWorkspacePath(workspaceRoot, targetPath));
|
|
@@ -123,7 +149,7 @@ async function restoreFeatureAssets(
|
|
|
123
149
|
workspaceRoot: string,
|
|
124
150
|
assets: readonly StaticFeatureAsset[],
|
|
125
151
|
changes: string[],
|
|
126
|
-
dryRun: boolean
|
|
152
|
+
dryRun: boolean,
|
|
127
153
|
): Promise<void> {
|
|
128
154
|
for (const asset of assets) {
|
|
129
155
|
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
@@ -133,7 +159,7 @@ async function restoreFeatureAssets(
|
|
|
133
159
|
|
|
134
160
|
if (!dryRun) {
|
|
135
161
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
136
|
-
await
|
|
162
|
+
await Bun.write(targetPath, Bun.file(asset.sourcePath));
|
|
137
163
|
if (asset.executable) {
|
|
138
164
|
await chmod(targetPath, 0o755);
|
|
139
165
|
}
|
|
@@ -143,7 +169,11 @@ async function restoreFeatureAssets(
|
|
|
143
169
|
}
|
|
144
170
|
}
|
|
145
171
|
|
|
146
|
-
async function restoreBackendAssets(
|
|
172
|
+
async function restoreBackendAssets(
|
|
173
|
+
workspaceRoot: string,
|
|
174
|
+
changes: string[],
|
|
175
|
+
dryRun: boolean,
|
|
176
|
+
): Promise<void> {
|
|
147
177
|
const assets = await getBackendScaffoldAssets();
|
|
148
178
|
for (const asset of assets) {
|
|
149
179
|
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
@@ -153,7 +183,7 @@ async function restoreBackendAssets(workspaceRoot: string, changes: string[], dr
|
|
|
153
183
|
|
|
154
184
|
if (!dryRun) {
|
|
155
185
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
156
|
-
await
|
|
186
|
+
await Bun.write(targetPath, Bun.file(asset.sourcePath));
|
|
157
187
|
}
|
|
158
188
|
|
|
159
189
|
changes.push(relativeWorkspacePath(workspaceRoot, targetPath));
|
|
@@ -164,21 +194,21 @@ async function ensureAppImport(
|
|
|
164
194
|
workspaceRoot: string,
|
|
165
195
|
importPath: string,
|
|
166
196
|
changes: string[],
|
|
167
|
-
dryRun: boolean
|
|
197
|
+
dryRun: boolean,
|
|
168
198
|
): Promise<void> {
|
|
169
199
|
const appTsPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.ts');
|
|
170
200
|
if (!existsSync(appTsPath)) {
|
|
171
201
|
return;
|
|
172
202
|
}
|
|
173
203
|
|
|
174
|
-
const source = await
|
|
204
|
+
const source = await readTextFile(appTsPath);
|
|
175
205
|
const updated = ensureSideEffectImport(source, importPath);
|
|
176
206
|
if (updated === source) {
|
|
177
207
|
return;
|
|
178
208
|
}
|
|
179
209
|
|
|
180
210
|
if (!dryRun) {
|
|
181
|
-
await
|
|
211
|
+
await Bun.write(appTsPath, updated);
|
|
182
212
|
}
|
|
183
213
|
changes.push(relativeWorkspacePath(workspaceRoot, appTsPath));
|
|
184
214
|
}
|
|
@@ -187,21 +217,21 @@ async function ensureCssLayerIncludes(
|
|
|
187
217
|
workspaceRoot: string,
|
|
188
218
|
layerName: string,
|
|
189
219
|
changes: string[],
|
|
190
|
-
dryRun: boolean
|
|
220
|
+
dryRun: boolean,
|
|
191
221
|
): Promise<void> {
|
|
192
222
|
const appCssPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.css');
|
|
193
223
|
if (!existsSync(appCssPath)) {
|
|
194
224
|
return;
|
|
195
225
|
}
|
|
196
226
|
|
|
197
|
-
const source = await
|
|
227
|
+
const source = await readTextFile(appCssPath);
|
|
198
228
|
const updated = ensureLayerIncludes(source, layerName);
|
|
199
229
|
if (updated === source) {
|
|
200
230
|
return;
|
|
201
231
|
}
|
|
202
232
|
|
|
203
233
|
if (!dryRun) {
|
|
204
|
-
await
|
|
234
|
+
await Bun.write(appCssPath, updated);
|
|
205
235
|
}
|
|
206
236
|
changes.push(relativeWorkspacePath(workspaceRoot, appCssPath));
|
|
207
237
|
}
|
|
@@ -211,63 +241,72 @@ async function ensureAppCssImport(
|
|
|
211
241
|
importPath: string,
|
|
212
242
|
insertAfterImportPath: string,
|
|
213
243
|
changes: string[],
|
|
214
|
-
dryRun: boolean
|
|
244
|
+
dryRun: boolean,
|
|
215
245
|
): Promise<void> {
|
|
216
246
|
const appCssPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.css');
|
|
217
247
|
if (!existsSync(appCssPath)) {
|
|
218
248
|
return;
|
|
219
249
|
}
|
|
220
250
|
|
|
221
|
-
const source = await
|
|
251
|
+
const source = await readTextFile(appCssPath);
|
|
222
252
|
const updated = ensureImportIncludes(source, importPath, insertAfterImportPath);
|
|
223
253
|
if (updated === source) {
|
|
224
254
|
return;
|
|
225
255
|
}
|
|
226
256
|
|
|
227
257
|
if (!dryRun) {
|
|
228
|
-
await
|
|
258
|
+
await Bun.write(appCssPath, updated);
|
|
229
259
|
}
|
|
230
260
|
changes.push(relativeWorkspacePath(workspaceRoot, appCssPath));
|
|
231
261
|
}
|
|
232
262
|
|
|
233
|
-
async function ensureHtmlSearchMode(
|
|
263
|
+
async function ensureHtmlSearchMode(
|
|
264
|
+
workspaceRoot: string,
|
|
265
|
+
changes: string[],
|
|
266
|
+
dryRun: boolean,
|
|
267
|
+
): Promise<void> {
|
|
234
268
|
const appHtmlPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.html');
|
|
235
269
|
if (!existsSync(appHtmlPath)) {
|
|
236
270
|
return;
|
|
237
271
|
}
|
|
238
272
|
|
|
239
|
-
const source = await
|
|
273
|
+
const source = await readTextFile(appHtmlPath);
|
|
240
274
|
if (source.includes('data-webstir-search-styles=')) {
|
|
241
275
|
return;
|
|
242
276
|
}
|
|
243
277
|
|
|
244
278
|
const updated = source.replace(
|
|
245
279
|
/<html\b(?![^>]*\bdata-webstir-search-styles=)/i,
|
|
246
|
-
'<html data-webstir-search-styles="css"'
|
|
280
|
+
'<html data-webstir-search-styles="css"',
|
|
247
281
|
);
|
|
248
282
|
if (updated === source) {
|
|
249
283
|
return;
|
|
250
284
|
}
|
|
251
285
|
|
|
252
286
|
if (!dryRun) {
|
|
253
|
-
await
|
|
287
|
+
await Bun.write(appHtmlPath, updated);
|
|
254
288
|
}
|
|
255
289
|
changes.push(relativeWorkspacePath(workspaceRoot, appHtmlPath));
|
|
256
290
|
}
|
|
257
291
|
|
|
258
|
-
async function ensureBackendTsReference(
|
|
292
|
+
async function ensureBackendTsReference(
|
|
293
|
+
workspaceRoot: string,
|
|
294
|
+
changes: string[],
|
|
295
|
+
dryRun: boolean,
|
|
296
|
+
): Promise<void> {
|
|
259
297
|
const tsconfigPath = path.join(workspaceRoot, 'base.tsconfig.json');
|
|
260
298
|
if (!existsSync(tsconfigPath)) {
|
|
261
299
|
return;
|
|
262
300
|
}
|
|
263
301
|
|
|
264
|
-
const source = await
|
|
302
|
+
const source = await readTextFile(tsconfigPath);
|
|
265
303
|
const root = JSON.parse(source) as Record<string, unknown>;
|
|
266
304
|
const references = Array.isArray(root.references) ? [...root.references] : [];
|
|
267
|
-
const exists = references.some(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
305
|
+
const exists = references.some(
|
|
306
|
+
(entry) =>
|
|
307
|
+
typeof entry === 'object' &&
|
|
308
|
+
entry !== null &&
|
|
309
|
+
(entry as Record<string, unknown>).path === 'src/backend',
|
|
271
310
|
);
|
|
272
311
|
if (exists) {
|
|
273
312
|
return;
|
|
@@ -278,12 +317,16 @@ async function ensureBackendTsReference(workspaceRoot: string, changes: string[]
|
|
|
278
317
|
const updated = `${JSON.stringify(root, null, 2)}\n`;
|
|
279
318
|
|
|
280
319
|
if (!dryRun) {
|
|
281
|
-
await
|
|
320
|
+
await Bun.write(tsconfigPath, updated);
|
|
282
321
|
}
|
|
283
322
|
changes.push(relativeWorkspacePath(workspaceRoot, tsconfigPath));
|
|
284
323
|
}
|
|
285
324
|
|
|
286
|
-
async function ensureGithubPagesDeployScript(
|
|
325
|
+
async function ensureGithubPagesDeployScript(
|
|
326
|
+
workspaceRoot: string,
|
|
327
|
+
changes: string[],
|
|
328
|
+
dryRun: boolean,
|
|
329
|
+
): Promise<void> {
|
|
287
330
|
const deployScriptPath = path.join(workspaceRoot, 'utils', 'deploy-gh-pages.sh');
|
|
288
331
|
if (existsSync(deployScriptPath)) {
|
|
289
332
|
return;
|
|
@@ -291,14 +334,18 @@ async function ensureGithubPagesDeployScript(workspaceRoot: string, changes: str
|
|
|
291
334
|
|
|
292
335
|
if (!dryRun) {
|
|
293
336
|
await mkdir(path.dirname(deployScriptPath), { recursive: true });
|
|
294
|
-
await
|
|
337
|
+
await Bun.write(deployScriptPath, renderGithubPagesDeployScript());
|
|
295
338
|
await chmod(deployScriptPath, 0o755);
|
|
296
339
|
}
|
|
297
340
|
changes.push(relativeWorkspacePath(workspaceRoot, deployScriptPath));
|
|
298
341
|
}
|
|
299
342
|
|
|
300
|
-
async function ensureDeployScriptEntry(
|
|
301
|
-
|
|
343
|
+
async function ensureDeployScriptEntry(
|
|
344
|
+
packageJsonPath: string,
|
|
345
|
+
changes: string[],
|
|
346
|
+
dryRun: boolean,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
const source = await readTextFile(packageJsonPath);
|
|
302
349
|
const root = JSON.parse(source) as RepairPackageJson & Record<string, unknown>;
|
|
303
350
|
const scripts = root.scripts && typeof root.scripts === 'object' ? { ...root.scripts } : {};
|
|
304
351
|
if (typeof scripts.deploy === 'string') {
|
|
@@ -310,18 +357,22 @@ async function ensureDeployScriptEntry(packageJsonPath: string, changes: string[
|
|
|
310
357
|
const updated = `${JSON.stringify(root, null, 2)}\n`;
|
|
311
358
|
|
|
312
359
|
if (!dryRun) {
|
|
313
|
-
await
|
|
360
|
+
await Bun.write(packageJsonPath, updated);
|
|
314
361
|
}
|
|
315
362
|
changes.push(path.basename(packageJsonPath));
|
|
316
363
|
}
|
|
317
364
|
|
|
318
|
-
async function ensureFrontendConfigBasePath(
|
|
365
|
+
async function ensureFrontendConfigBasePath(
|
|
366
|
+
workspaceRoot: string,
|
|
367
|
+
changes: string[],
|
|
368
|
+
dryRun: boolean,
|
|
369
|
+
): Promise<void> {
|
|
319
370
|
const configPath = path.join(workspaceRoot, 'src', 'frontend', 'frontend.config.json');
|
|
320
371
|
let root: Record<string, unknown> = {};
|
|
321
372
|
|
|
322
373
|
if (existsSync(configPath)) {
|
|
323
374
|
try {
|
|
324
|
-
root = JSON.parse(await
|
|
375
|
+
root = JSON.parse(await readTextFile(configPath)) as Record<string, unknown>;
|
|
325
376
|
} catch {
|
|
326
377
|
root = {};
|
|
327
378
|
}
|
|
@@ -338,11 +389,15 @@ async function ensureFrontendConfigBasePath(workspaceRoot: string, changes: stri
|
|
|
338
389
|
|
|
339
390
|
if (!dryRun) {
|
|
340
391
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
341
|
-
await
|
|
392
|
+
await Bun.write(configPath, updated);
|
|
342
393
|
}
|
|
343
394
|
changes.push(relativeWorkspacePath(workspaceRoot, configPath));
|
|
344
395
|
}
|
|
345
396
|
|
|
397
|
+
async function readTextFile(filePath: string): Promise<string> {
|
|
398
|
+
return await Bun.file(filePath).text();
|
|
399
|
+
}
|
|
400
|
+
|
|
346
401
|
function ensureSideEffectImport(source: string, importPath: string): string {
|
|
347
402
|
const escaped = escapeRegExp(importPath);
|
|
348
403
|
const pattern = new RegExp(`^\\s*import\\s+(['"])${escaped}\\1\\s*;?\\s*$`, 'm');
|
|
@@ -360,7 +415,10 @@ function ensureLayerIncludes(css: string, layerName: string): string {
|
|
|
360
415
|
return css;
|
|
361
416
|
}
|
|
362
417
|
|
|
363
|
-
const layers = match[1]
|
|
418
|
+
const layers = match[1]
|
|
419
|
+
.split(',')
|
|
420
|
+
.map((layer) => layer.trim())
|
|
421
|
+
.filter(Boolean);
|
|
364
422
|
if (layers.includes(layerName)) {
|
|
365
423
|
return css;
|
|
366
424
|
}
|
|
@@ -368,13 +426,18 @@ function ensureLayerIncludes(css: string, layerName: string): string {
|
|
|
368
426
|
const updated = [...layers];
|
|
369
427
|
const utilitiesIndex = updated.indexOf('utilities');
|
|
370
428
|
const overridesIndex = updated.indexOf('overrides');
|
|
371
|
-
const insertIndex =
|
|
429
|
+
const insertIndex =
|
|
430
|
+
utilitiesIndex >= 0 ? utilitiesIndex : overridesIndex >= 0 ? overridesIndex : updated.length;
|
|
372
431
|
updated.splice(insertIndex, 0, layerName);
|
|
373
432
|
const replacement = `@layer ${updated.join(', ')};`;
|
|
374
433
|
return `${css.slice(0, match.index)}${replacement}${css.slice(match.index + match[0].length)}`;
|
|
375
434
|
}
|
|
376
435
|
|
|
377
|
-
function ensureImportIncludes(
|
|
436
|
+
function ensureImportIncludes(
|
|
437
|
+
css: string,
|
|
438
|
+
importPath: string,
|
|
439
|
+
insertAfterImportPath: string,
|
|
440
|
+
): string {
|
|
378
441
|
if (css.includes(`@import "${importPath}"`) || css.includes(`@import '${importPath}'`)) {
|
|
379
442
|
return css;
|
|
380
443
|
}
|
|
@@ -399,6 +462,10 @@ function relativeWorkspacePath(workspaceRoot: string, absolutePath: string): str
|
|
|
399
462
|
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, '/');
|
|
400
463
|
}
|
|
401
464
|
|
|
465
|
+
function normalizeRelativePath(value: string): string {
|
|
466
|
+
return value.split(path.sep).join('/');
|
|
467
|
+
}
|
|
468
|
+
|
|
402
469
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
403
470
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
404
471
|
? { ...(value as Record<string, unknown>) }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TestManifest } from '@webstir-io/webstir-testing';
|
|
2
|
+
|
|
3
|
+
export type RuntimeFilter = 'frontend' | 'backend' | null;
|
|
4
|
+
|
|
5
|
+
export function normalizeRuntimeFilter(value: string | undefined | null): RuntimeFilter {
|
|
6
|
+
const normalized = (value ?? '').trim().toLowerCase();
|
|
7
|
+
if (!normalized || normalized === 'all') {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (normalized === 'frontend' || normalized === 'backend') {
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function applyRuntimeFilter(manifest: TestManifest, runtime: RuntimeFilter): TestManifest {
|
|
19
|
+
if (!runtime) {
|
|
20
|
+
return manifest;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
...manifest,
|
|
25
|
+
modules: manifest.modules.filter((module) => module.runtime === runtime),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function describeRuntimeFilter(
|
|
30
|
+
runtime: RuntimeFilter,
|
|
31
|
+
before: number,
|
|
32
|
+
after: number,
|
|
33
|
+
): string | null {
|
|
34
|
+
if (!runtime) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const skipped = Math.max(before - after, 0);
|
|
39
|
+
const noun = after === 1 ? 'test' : 'tests';
|
|
40
|
+
return `Runtime filter '${runtime}' matched ${after} ${noun} (${skipped} skipped).`;
|
|
41
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type ModuleRuntimeMode = 'build' | 'publish' | 'test';
|
|
|
7
7
|
export function createWorkspaceRuntimeEnv(
|
|
8
8
|
workspaceRoot: string,
|
|
9
9
|
mode: ModuleRuntimeMode,
|
|
10
|
-
env: Record<string, string | undefined> = process.env
|
|
10
|
+
env: Record<string, string | undefined> = process.env,
|
|
11
11
|
): Record<string, string | undefined> {
|
|
12
12
|
const binPaths = [
|
|
13
13
|
...collectNodeModuleBins(workspaceRoot),
|
package/src/smoke.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
4
|
|
|
5
5
|
import type { WorkspaceDescriptor } from './types.ts';
|
|
6
6
|
|
|
7
7
|
import { runBackendInspect, type BackendInspectResult } from './backend-inspect.ts';
|
|
8
|
+
import { runDoctor, type DoctorResult } from './doctor.ts';
|
|
9
|
+
import {
|
|
10
|
+
materializeRepoLocalWorkspaceDependencies,
|
|
11
|
+
prepareExternalWorkspaceCopy,
|
|
12
|
+
} from './external-workspace.ts';
|
|
8
13
|
import { monorepoRoot } from './paths.ts';
|
|
9
14
|
import { runBuild, type RunBuildOptions } from './build.ts';
|
|
10
15
|
import { scaffoldWorkspace } from './init.ts';
|
|
@@ -18,7 +23,7 @@ export interface RunSmokeOptions {
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export interface SmokePhaseResult {
|
|
21
|
-
readonly name: 'build' | 'test' | 'publish' | 'backend-inspect';
|
|
26
|
+
readonly name: 'build' | 'test' | 'publish' | 'doctor' | 'backend-inspect';
|
|
22
27
|
readonly detail: string;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -68,6 +73,18 @@ export async function runSmoke(options: RunSmokeOptions = {}): Promise<SmokeResu
|
|
|
68
73
|
detail: formatBuildDetail(publishResult),
|
|
69
74
|
});
|
|
70
75
|
|
|
76
|
+
const doctorResult = await runDoctor({
|
|
77
|
+
workspaceRoot: workspace.root,
|
|
78
|
+
env,
|
|
79
|
+
});
|
|
80
|
+
if (!doctorResult.healthy) {
|
|
81
|
+
throw new Error('Smoke doctor phase reported issues.');
|
|
82
|
+
}
|
|
83
|
+
phases.push({
|
|
84
|
+
name: 'doctor',
|
|
85
|
+
detail: formatDoctorDetail(doctorResult),
|
|
86
|
+
});
|
|
87
|
+
|
|
71
88
|
if (workspace.mode === 'api' || workspace.mode === 'full') {
|
|
72
89
|
const backendInspect = await runBackendInspect({
|
|
73
90
|
workspaceRoot: workspace.root,
|
|
@@ -94,14 +111,15 @@ export async function runSmoke(options: RunSmokeOptions = {}): Promise<SmokeResu
|
|
|
94
111
|
|
|
95
112
|
function createSmokeEnv(
|
|
96
113
|
workspaceRoot: string,
|
|
97
|
-
env: Record<string, string | undefined> = process.env
|
|
114
|
+
env: Record<string, string | undefined> = process.env,
|
|
98
115
|
): Record<string, string | undefined> {
|
|
99
116
|
if (env.WEBSTIR_BACKEND_TYPECHECK) {
|
|
100
117
|
return env;
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
const relativeToRepo = monorepoRoot ? path.relative(monorepoRoot, workspaceRoot) : '../external';
|
|
104
|
-
const isExternalWorkspace =
|
|
121
|
+
const isExternalWorkspace =
|
|
122
|
+
!monorepoRoot || relativeToRepo.startsWith('..') || path.isAbsolute(relativeToRepo);
|
|
105
123
|
if (!isExternalWorkspace) {
|
|
106
124
|
return env;
|
|
107
125
|
}
|
|
@@ -119,30 +137,40 @@ async function prepareWorkspace(workspaceRoot?: string): Promise<{
|
|
|
119
137
|
readonly source?: string;
|
|
120
138
|
}> {
|
|
121
139
|
if (workspaceRoot) {
|
|
140
|
+
const resolvedWorkspaceRoot = path.resolve(workspaceRoot);
|
|
141
|
+
const preparedExternalWorkspace = await prepareExternalWorkspaceCopy(
|
|
142
|
+
resolvedWorkspaceRoot,
|
|
143
|
+
'webstir-smoke-explicit-',
|
|
144
|
+
{
|
|
145
|
+
forceLocalPackages: true,
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
if (preparedExternalWorkspace) {
|
|
149
|
+
return {
|
|
150
|
+
workspaceRoot: preparedExternalWorkspace.workspaceRoot,
|
|
151
|
+
cleanupRoot: preparedExternalWorkspace.cleanupRoot,
|
|
152
|
+
usedTempWorkspace: false,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
122
156
|
return {
|
|
123
|
-
workspaceRoot:
|
|
157
|
+
workspaceRoot: resolvedWorkspaceRoot,
|
|
124
158
|
usedTempWorkspace: false,
|
|
125
159
|
};
|
|
126
160
|
}
|
|
127
161
|
|
|
128
162
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'webstir-smoke-'));
|
|
129
163
|
const tempWorkspace = path.join(tempRoot, 'full');
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
await cp(sourceRoot, tempWorkspace, { recursive: true });
|
|
135
|
-
source = sourceRoot;
|
|
136
|
-
} else {
|
|
137
|
-
await scaffoldWorkspace('full', tempWorkspace, { force: true });
|
|
138
|
-
source = 'built-in full template';
|
|
139
|
-
}
|
|
164
|
+
await scaffoldWorkspace('full', tempWorkspace, { force: true });
|
|
165
|
+
await materializeRepoLocalWorkspaceDependencies(tempWorkspace, {
|
|
166
|
+
forceLocalPackages: true,
|
|
167
|
+
});
|
|
140
168
|
|
|
141
169
|
return {
|
|
142
170
|
workspaceRoot: tempWorkspace,
|
|
143
171
|
cleanupRoot: tempRoot,
|
|
144
172
|
usedTempWorkspace: true,
|
|
145
|
-
source,
|
|
173
|
+
source: 'built-in full template',
|
|
146
174
|
};
|
|
147
175
|
}
|
|
148
176
|
|
|
@@ -159,3 +187,7 @@ function formatTestDetail(result: TestCommandResult): string {
|
|
|
159
187
|
function formatBackendInspectDetail(result: BackendInspectResult): string {
|
|
160
188
|
return `${result.manifest.routes?.length ?? 0} routes, ${result.manifest.jobs?.length ?? 0} jobs`;
|
|
161
189
|
}
|
|
190
|
+
|
|
191
|
+
function formatDoctorDetail(result: DoctorResult): string {
|
|
192
|
+
return result.healthy ? 'healthy' : `${result.issues.length} issue(s)`;
|
|
193
|
+
}
|