@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/doctor.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { BackendInspectResult } from './backend-inspect.ts';
|
|
2
|
+
import type { WorkspaceDescriptor } from './types.ts';
|
|
3
|
+
|
|
4
|
+
import { runBackendInspect } from './backend-inspect.ts';
|
|
5
|
+
import { runRepair } from './repair.ts';
|
|
6
|
+
import { readWorkspaceDescriptor } from './workspace.ts';
|
|
7
|
+
|
|
8
|
+
export interface RunDoctorOptions {
|
|
9
|
+
readonly workspaceRoot: string;
|
|
10
|
+
readonly env?: Record<string, string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DoctorCheck {
|
|
14
|
+
readonly id: 'scaffold' | 'backend-inspect';
|
|
15
|
+
readonly status: 'pass' | 'fail' | 'skip';
|
|
16
|
+
readonly summary: string;
|
|
17
|
+
readonly detail?: string;
|
|
18
|
+
readonly changes?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DoctorIssue {
|
|
22
|
+
readonly code: 'scaffold_drift' | 'backend_inspect_failed';
|
|
23
|
+
readonly severity: 'error';
|
|
24
|
+
readonly message: string;
|
|
25
|
+
readonly repairable: boolean;
|
|
26
|
+
readonly changes?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DoctorRepairPlan {
|
|
30
|
+
readonly command: 'repair';
|
|
31
|
+
readonly args: readonly string[];
|
|
32
|
+
readonly changes: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DoctorBackendSummary {
|
|
36
|
+
readonly buildRoot: string;
|
|
37
|
+
readonly module: string;
|
|
38
|
+
readonly routes: number;
|
|
39
|
+
readonly jobs: number;
|
|
40
|
+
readonly data: DoctorBackendDataSummary;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DoctorBackendDataSummary {
|
|
44
|
+
readonly migrations: DoctorBackendMigrationSummary;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DoctorBackendMigrationSummary {
|
|
48
|
+
readonly runnerPresent: boolean;
|
|
49
|
+
readonly migrationsDirectoryPresent: boolean;
|
|
50
|
+
readonly migrationFilesCount: number;
|
|
51
|
+
readonly exampleMigrationPresent: boolean;
|
|
52
|
+
readonly tableEnvKey: string;
|
|
53
|
+
readonly configuredTable: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DoctorResult {
|
|
57
|
+
readonly workspace: WorkspaceDescriptor;
|
|
58
|
+
readonly healthy: boolean;
|
|
59
|
+
readonly checks: readonly DoctorCheck[];
|
|
60
|
+
readonly issues: readonly DoctorIssue[];
|
|
61
|
+
readonly repair: DoctorRepairPlan;
|
|
62
|
+
readonly backend?: DoctorBackendSummary;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runDoctor(options: RunDoctorOptions): Promise<DoctorResult> {
|
|
66
|
+
const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
|
|
67
|
+
const checks: DoctorCheck[] = [];
|
|
68
|
+
const issues: DoctorIssue[] = [];
|
|
69
|
+
|
|
70
|
+
const repairResult = await runRepair({
|
|
71
|
+
workspaceRoot: workspace.root,
|
|
72
|
+
rawArgs: ['--dry-run'],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (repairResult.changes.length > 0) {
|
|
76
|
+
checks.push({
|
|
77
|
+
id: 'scaffold',
|
|
78
|
+
status: 'fail',
|
|
79
|
+
summary: `${repairResult.changes.length} scaffold-managed change(s) required.`,
|
|
80
|
+
changes: repairResult.changes,
|
|
81
|
+
});
|
|
82
|
+
issues.push({
|
|
83
|
+
code: 'scaffold_drift',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
message: 'Scaffold-managed files or wiring have drifted from the expected workspace shape.',
|
|
86
|
+
repairable: true,
|
|
87
|
+
changes: repairResult.changes,
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
checks.push({
|
|
91
|
+
id: 'scaffold',
|
|
92
|
+
status: 'pass',
|
|
93
|
+
summary: 'Scaffold-managed files and wiring match the expected workspace shape.',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let backend: DoctorBackendSummary | undefined;
|
|
98
|
+
if (workspace.mode === 'api' || workspace.mode === 'full') {
|
|
99
|
+
try {
|
|
100
|
+
const inspectResult = await runBackendInspect({
|
|
101
|
+
workspaceRoot: workspace.root,
|
|
102
|
+
env: options.env,
|
|
103
|
+
});
|
|
104
|
+
backend = summarizeBackendInspect(inspectResult);
|
|
105
|
+
checks.push({
|
|
106
|
+
id: 'backend-inspect',
|
|
107
|
+
status: 'pass',
|
|
108
|
+
summary: `${backend.routes} route(s), ${backend.jobs} job(s), module ${backend.module}.`,
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
checks.push({
|
|
113
|
+
id: 'backend-inspect',
|
|
114
|
+
status: 'fail',
|
|
115
|
+
summary: 'Backend inspect failed.',
|
|
116
|
+
detail: message,
|
|
117
|
+
});
|
|
118
|
+
issues.push({
|
|
119
|
+
code: 'backend_inspect_failed',
|
|
120
|
+
severity: 'error',
|
|
121
|
+
message: `Backend inspect failed: ${message}`,
|
|
122
|
+
repairable: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
checks.push({
|
|
127
|
+
id: 'backend-inspect',
|
|
128
|
+
status: 'skip',
|
|
129
|
+
summary: `Skipped for ${workspace.mode} workspaces.`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
workspace,
|
|
135
|
+
healthy: issues.length === 0,
|
|
136
|
+
checks,
|
|
137
|
+
issues,
|
|
138
|
+
repair: {
|
|
139
|
+
command: 'repair',
|
|
140
|
+
args: ['--workspace', workspace.root],
|
|
141
|
+
changes: repairResult.changes,
|
|
142
|
+
},
|
|
143
|
+
...(backend ? { backend } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function summarizeBackendInspect(result: BackendInspectResult): DoctorBackendSummary {
|
|
148
|
+
return {
|
|
149
|
+
buildRoot: result.buildRoot,
|
|
150
|
+
module: `${result.manifest.name}@${result.manifest.version}`,
|
|
151
|
+
routes: result.manifest.routes?.length ?? 0,
|
|
152
|
+
jobs: result.manifest.jobs?.length ?? 0,
|
|
153
|
+
data: {
|
|
154
|
+
migrations: {
|
|
155
|
+
runnerPresent: result.data.migrations.runnerPresent,
|
|
156
|
+
migrationsDirectoryPresent: result.data.migrations.migrationsDirectoryPresent,
|
|
157
|
+
migrationFilesCount: result.data.migrations.migrationFilesCount,
|
|
158
|
+
exampleMigrationPresent: result.data.migrations.exampleMigrationPresent,
|
|
159
|
+
tableEnvKey: result.data.migrations.tableEnvKey,
|
|
160
|
+
configuredTable: result.data.migrations.configuredTable,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
package/src/enable-assets.ts
CHANGED
|
@@ -33,6 +33,23 @@ export function getClientNavAssets(): readonly StaticFeatureAsset[] {
|
|
|
33
33
|
targetPath: path.join('src', 'frontend', 'app', 'scripts', 'features', 'client-nav.ts'),
|
|
34
34
|
overwrite: true,
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
sourcePath: path.join(featuresRoot, 'client_nav', 'form_enhancement.ts'),
|
|
38
|
+
targetPath: path.join('src', 'frontend', 'app', 'scripts', 'features', 'form-enhancement.ts'),
|
|
39
|
+
overwrite: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
sourcePath: path.join(featuresRoot, 'client_nav', 'document_navigation.ts'),
|
|
43
|
+
targetPath: path.join(
|
|
44
|
+
'src',
|
|
45
|
+
'frontend',
|
|
46
|
+
'app',
|
|
47
|
+
'scripts',
|
|
48
|
+
'features',
|
|
49
|
+
'document-navigation.ts',
|
|
50
|
+
),
|
|
51
|
+
overwrite: true,
|
|
52
|
+
},
|
|
36
53
|
];
|
|
37
54
|
}
|
|
38
55
|
|
|
@@ -185,7 +202,7 @@ jobs:
|
|
|
185
202
|
- name: Setup Bun
|
|
186
203
|
uses: oven-sh/setup-bun@v2
|
|
187
204
|
with:
|
|
188
|
-
bun-version: 1.3.
|
|
205
|
+
bun-version: 1.3.11
|
|
189
206
|
|
|
190
207
|
- name: Install dependencies
|
|
191
208
|
run: bun install --frozen-lockfile
|
package/src/enable.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { chmod,
|
|
2
|
+
import { chmod, mkdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
import { getBackendScaffoldAssets } from '@webstir-io/webstir-backend';
|
|
6
7
|
import {
|
|
@@ -42,7 +43,7 @@ export async function runEnable(options: RunEnableOptions): Promise<EnableResult
|
|
|
42
43
|
const [featureToken, ...rest] = options.args;
|
|
43
44
|
if (!featureToken) {
|
|
44
45
|
throw new Error(
|
|
45
|
-
'Missing enable feature. Usage: webstir enable <scripts <page>|spa|client-nav|search|content-nav|backend|github-pages|gh-deploy> --workspace <path>.'
|
|
46
|
+
'Missing enable feature. Usage: webstir enable <scripts <page>|spa|client-nav|search|content-nav|backend|github-pages|gh-deploy> --workspace <path>.',
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -80,10 +81,22 @@ export async function runEnable(options: RunEnableOptions): Promise<EnableResult
|
|
|
80
81
|
break;
|
|
81
82
|
case 'github-pages':
|
|
82
83
|
case 'gh-pages':
|
|
83
|
-
await enableGithubPages(
|
|
84
|
+
await enableGithubPages(
|
|
85
|
+
workspace.root,
|
|
86
|
+
path.basename(workspace.root),
|
|
87
|
+
rest[0],
|
|
88
|
+
false,
|
|
89
|
+
changes,
|
|
90
|
+
);
|
|
84
91
|
break;
|
|
85
92
|
case 'gh-deploy':
|
|
86
|
-
await enableGithubPages(
|
|
93
|
+
await enableGithubPages(
|
|
94
|
+
workspace.root,
|
|
95
|
+
path.basename(workspace.root),
|
|
96
|
+
rest[0],
|
|
97
|
+
true,
|
|
98
|
+
changes,
|
|
99
|
+
);
|
|
87
100
|
break;
|
|
88
101
|
}
|
|
89
102
|
|
|
@@ -109,12 +122,16 @@ function parseEnableFeature(value: string): EnableFeature {
|
|
|
109
122
|
return normalized;
|
|
110
123
|
default:
|
|
111
124
|
throw new Error(
|
|
112
|
-
`Unknown feature "${value}". Expected scripts, spa, client-nav, search, content-nav, backend, github-pages, or gh-deploy
|
|
125
|
+
`Unknown feature "${value}". Expected scripts, spa, client-nav, search, content-nav, backend, github-pages, or gh-deploy.`,
|
|
113
126
|
);
|
|
114
127
|
}
|
|
115
128
|
}
|
|
116
129
|
|
|
117
|
-
async function enableScripts(
|
|
130
|
+
async function enableScripts(
|
|
131
|
+
workspaceRoot: string,
|
|
132
|
+
args: readonly string[],
|
|
133
|
+
changes: string[],
|
|
134
|
+
): Promise<void> {
|
|
118
135
|
const pageName = args[0];
|
|
119
136
|
if (!pageName) {
|
|
120
137
|
throw new Error('Usage: webstir enable scripts <page> --workspace <path>.');
|
|
@@ -137,7 +154,7 @@ async function enableScripts(workspaceRoot: string, args: readonly string[], cha
|
|
|
137
154
|
async function copyStaticAssets(
|
|
138
155
|
workspaceRoot: string,
|
|
139
156
|
assets: readonly StaticFeatureAsset[],
|
|
140
|
-
changes: string[]
|
|
157
|
+
changes: string[],
|
|
141
158
|
): Promise<void> {
|
|
142
159
|
for (const asset of assets) {
|
|
143
160
|
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
@@ -151,7 +168,7 @@ async function copyStaticAssets(
|
|
|
151
168
|
continue;
|
|
152
169
|
}
|
|
153
170
|
|
|
154
|
-
await
|
|
171
|
+
await Bun.write(targetPath, Bun.file(asset.sourcePath));
|
|
155
172
|
if (asset.executable) {
|
|
156
173
|
await chmod(targetPath, 0o755);
|
|
157
174
|
}
|
|
@@ -166,12 +183,16 @@ async function enableBackend(workspaceRoot: string, changes: string[]): Promise<
|
|
|
166
183
|
for (const asset of assets) {
|
|
167
184
|
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
168
185
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
169
|
-
await
|
|
186
|
+
await Bun.write(targetPath, Bun.file(asset.sourcePath));
|
|
170
187
|
changes.push(relativeWorkspacePath(workspaceRoot, targetPath));
|
|
171
188
|
}
|
|
172
189
|
}
|
|
173
190
|
|
|
174
|
-
await updatePackageJson(
|
|
191
|
+
await updatePackageJson(
|
|
192
|
+
workspaceRoot,
|
|
193
|
+
{ enableBackend: true, ensureBackendDependency: true, mode: 'full' },
|
|
194
|
+
changes,
|
|
195
|
+
);
|
|
175
196
|
await ensureTsReference(workspaceRoot, 'src/backend', changes);
|
|
176
197
|
}
|
|
177
198
|
|
|
@@ -180,7 +201,7 @@ async function enableGithubPages(
|
|
|
180
201
|
workspaceName: string,
|
|
181
202
|
basePathArg: string | undefined,
|
|
182
203
|
includeWorkflow: boolean,
|
|
183
|
-
changes: string[]
|
|
204
|
+
changes: string[],
|
|
184
205
|
): Promise<void> {
|
|
185
206
|
const resolvedBasePath = resolveGithubPagesBasePath(basePathArg, workspaceName);
|
|
186
207
|
const deployScriptPath = path.join(workspaceRoot, 'utils', 'deploy-gh-pages.sh');
|
|
@@ -196,40 +217,44 @@ async function enableGithubPages(
|
|
|
196
217
|
}
|
|
197
218
|
|
|
198
219
|
await updateFrontendConfig(workspaceRoot, resolvedBasePath, changes);
|
|
199
|
-
await updatePackageJson(
|
|
220
|
+
await updatePackageJson(
|
|
221
|
+
workspaceRoot,
|
|
222
|
+
{ enableGithubPages: true, ensureDeployScript: true },
|
|
223
|
+
changes,
|
|
224
|
+
);
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
async function ensureAppScriptImport(
|
|
203
228
|
workspaceRoot: string,
|
|
204
229
|
importPath: string,
|
|
205
|
-
changes: string[]
|
|
230
|
+
changes: string[],
|
|
206
231
|
): Promise<void> {
|
|
207
232
|
const appTsPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.ts');
|
|
208
233
|
if (!existsSync(appTsPath)) {
|
|
209
234
|
return;
|
|
210
235
|
}
|
|
211
236
|
|
|
212
|
-
const source = await
|
|
237
|
+
const source = await readTextFile(appTsPath);
|
|
213
238
|
const updated = ensureSideEffectImport(source, importPath);
|
|
214
239
|
if (updated === source) {
|
|
215
240
|
return;
|
|
216
241
|
}
|
|
217
242
|
|
|
218
|
-
await
|
|
243
|
+
await Bun.write(appTsPath, updated);
|
|
219
244
|
changes.push(relativeWorkspacePath(workspaceRoot, appTsPath));
|
|
220
245
|
}
|
|
221
246
|
|
|
222
247
|
async function ensureAppCssImport(
|
|
223
248
|
workspaceRoot: string,
|
|
224
249
|
importPath: string,
|
|
225
|
-
changes: string[]
|
|
250
|
+
changes: string[],
|
|
226
251
|
): Promise<void> {
|
|
227
252
|
const appCssPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.css');
|
|
228
253
|
if (!existsSync(appCssPath)) {
|
|
229
254
|
return;
|
|
230
255
|
}
|
|
231
256
|
|
|
232
|
-
const source = await
|
|
257
|
+
const source = await readTextFile(appCssPath);
|
|
233
258
|
let updated = source;
|
|
234
259
|
updated = ensureLayerIncludes(updated, 'features');
|
|
235
260
|
updated = ensureImportIncludes(updated, importPath, './styles/components/buttons.css');
|
|
@@ -237,7 +262,7 @@ async function ensureAppCssImport(
|
|
|
237
262
|
return;
|
|
238
263
|
}
|
|
239
264
|
|
|
240
|
-
await
|
|
265
|
+
await Bun.write(appCssPath, updated);
|
|
241
266
|
changes.push(relativeWorkspacePath(workspaceRoot, appCssPath));
|
|
242
267
|
}
|
|
243
268
|
|
|
@@ -247,20 +272,20 @@ async function ensureHtmlSearchMode(workspaceRoot: string, changes: string[]): P
|
|
|
247
272
|
return;
|
|
248
273
|
}
|
|
249
274
|
|
|
250
|
-
const source = await
|
|
275
|
+
const source = await readTextFile(appHtmlPath);
|
|
251
276
|
if (source.includes('data-webstir-search-styles=')) {
|
|
252
277
|
return;
|
|
253
278
|
}
|
|
254
279
|
|
|
255
280
|
const updated = source.replace(
|
|
256
281
|
/<html\b(?![^>]*\bdata-webstir-search-styles=)/i,
|
|
257
|
-
'<html data-webstir-search-styles="css"'
|
|
282
|
+
'<html data-webstir-search-styles="css"',
|
|
258
283
|
);
|
|
259
284
|
if (updated === source) {
|
|
260
285
|
return;
|
|
261
286
|
}
|
|
262
287
|
|
|
263
|
-
await
|
|
288
|
+
await Bun.write(appHtmlPath, updated);
|
|
264
289
|
changes.push(relativeWorkspacePath(workspaceRoot, appHtmlPath));
|
|
265
290
|
}
|
|
266
291
|
|
|
@@ -274,12 +299,13 @@ async function updatePackageJson(
|
|
|
274
299
|
readonly enableBackend?: boolean;
|
|
275
300
|
readonly enableGithubPages?: boolean;
|
|
276
301
|
readonly mode?: string;
|
|
302
|
+
readonly ensureBackendDependency?: boolean;
|
|
277
303
|
readonly ensureDeployScript?: boolean;
|
|
278
304
|
},
|
|
279
|
-
changes: string[]
|
|
305
|
+
changes: string[],
|
|
280
306
|
): Promise<void> {
|
|
281
307
|
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
282
|
-
const source = await
|
|
308
|
+
const source = await readTextFile(packageJsonPath);
|
|
283
309
|
const root = JSON.parse(source) as Record<string, unknown>;
|
|
284
310
|
const webstir = asRecord(root.webstir);
|
|
285
311
|
const enable = asRecord(webstir.enable);
|
|
@@ -309,6 +335,10 @@ async function updatePackageJson(
|
|
|
309
335
|
webstir.enable = enable;
|
|
310
336
|
root.webstir = webstir;
|
|
311
337
|
|
|
338
|
+
if (options.ensureBackendDependency) {
|
|
339
|
+
await ensureBackendScaffoldDependencies(root);
|
|
340
|
+
}
|
|
341
|
+
|
|
312
342
|
if (options.ensureDeployScript) {
|
|
313
343
|
const scripts = asRecord(root.scripts);
|
|
314
344
|
if (typeof scripts.deploy !== 'string') {
|
|
@@ -322,17 +352,61 @@ async function updatePackageJson(
|
|
|
322
352
|
return;
|
|
323
353
|
}
|
|
324
354
|
|
|
325
|
-
await
|
|
355
|
+
await Bun.write(packageJsonPath, updated);
|
|
326
356
|
changes.push(relativeWorkspacePath(workspaceRoot, packageJsonPath));
|
|
327
357
|
}
|
|
328
358
|
|
|
329
|
-
async function
|
|
359
|
+
async function ensureBackendScaffoldDependencies(root: Record<string, unknown>): Promise<void> {
|
|
360
|
+
const dependencies = asRecord(root.dependencies);
|
|
361
|
+
if (typeof dependencies['@webstir-io/webstir-backend'] !== 'string') {
|
|
362
|
+
dependencies['@webstir-io/webstir-backend'] = await resolveBackendDependencySpec(root);
|
|
363
|
+
}
|
|
364
|
+
if (typeof dependencies.pino !== 'string') {
|
|
365
|
+
dependencies.pino = '^10.1.0';
|
|
366
|
+
}
|
|
367
|
+
root.dependencies = dependencies;
|
|
368
|
+
|
|
369
|
+
const devDependencies = asRecord(root.devDependencies);
|
|
370
|
+
if (typeof devDependencies['@types/bun'] !== 'string') {
|
|
371
|
+
devDependencies['@types/bun'] = '^1.3.11';
|
|
372
|
+
}
|
|
373
|
+
root.devDependencies = devDependencies;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function resolveBackendDependencySpec(root: Record<string, unknown>): Promise<string> {
|
|
377
|
+
const dependencies = asRecord(root.dependencies);
|
|
378
|
+
const frontendSpec = dependencies['@webstir-io/webstir-frontend'];
|
|
379
|
+
if (typeof frontendSpec === 'string' && frontendSpec.startsWith('workspace:')) {
|
|
380
|
+
return 'workspace:*';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return await readInstalledPackageVersion('@webstir-io/webstir-backend');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function readInstalledPackageVersion(packageName: string): Promise<string> {
|
|
387
|
+
const packageJsonUrl = import.meta.resolve(`${packageName}/package.json`);
|
|
388
|
+
const packageJsonPath = fileURLToPath(packageJsonUrl);
|
|
389
|
+
const packageJson = JSON.parse(await readTextFile(packageJsonPath)) as {
|
|
390
|
+
readonly version?: string;
|
|
391
|
+
};
|
|
392
|
+
if (!packageJson.version) {
|
|
393
|
+
throw new Error(`Missing version in ${packageJsonPath}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return `^${packageJson.version}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function updateFrontendConfig(
|
|
400
|
+
workspaceRoot: string,
|
|
401
|
+
basePath: string,
|
|
402
|
+
changes: string[],
|
|
403
|
+
): Promise<void> {
|
|
330
404
|
const configPath = path.join(workspaceRoot, 'src', 'frontend', 'frontend.config.json');
|
|
331
405
|
let root: Record<string, unknown> = {};
|
|
332
406
|
|
|
333
407
|
if (existsSync(configPath)) {
|
|
334
408
|
try {
|
|
335
|
-
root = JSON.parse(await
|
|
409
|
+
root = JSON.parse(await readTextFile(configPath)) as Record<string, unknown>;
|
|
336
410
|
} catch {
|
|
337
411
|
root = {};
|
|
338
412
|
}
|
|
@@ -343,7 +417,7 @@ async function updateFrontendConfig(workspaceRoot: string, basePath: string, cha
|
|
|
343
417
|
root.publish = publish;
|
|
344
418
|
|
|
345
419
|
const updated = `${JSON.stringify(root, null, 2)}\n`;
|
|
346
|
-
const current = existsSync(configPath) ? await
|
|
420
|
+
const current = existsSync(configPath) ? await readTextFile(configPath) : null;
|
|
347
421
|
if (current === updated) {
|
|
348
422
|
return;
|
|
349
423
|
}
|
|
@@ -352,19 +426,24 @@ async function updateFrontendConfig(workspaceRoot: string, basePath: string, cha
|
|
|
352
426
|
changes.push(relativeWorkspacePath(workspaceRoot, configPath));
|
|
353
427
|
}
|
|
354
428
|
|
|
355
|
-
async function ensureTsReference(
|
|
429
|
+
async function ensureTsReference(
|
|
430
|
+
workspaceRoot: string,
|
|
431
|
+
referencePath: string,
|
|
432
|
+
changes: string[],
|
|
433
|
+
): Promise<void> {
|
|
356
434
|
const tsconfigPath = path.join(workspaceRoot, 'base.tsconfig.json');
|
|
357
435
|
if (!existsSync(tsconfigPath)) {
|
|
358
436
|
return;
|
|
359
437
|
}
|
|
360
438
|
|
|
361
|
-
const source = await
|
|
439
|
+
const source = await readTextFile(tsconfigPath);
|
|
362
440
|
const root = JSON.parse(source) as Record<string, unknown>;
|
|
363
441
|
const references = Array.isArray(root.references) ? [...root.references] : [];
|
|
364
|
-
const exists = references.some(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
442
|
+
const exists = references.some(
|
|
443
|
+
(entry) =>
|
|
444
|
+
typeof entry === 'object' &&
|
|
445
|
+
entry !== null &&
|
|
446
|
+
(entry as Record<string, unknown>).path === referencePath,
|
|
368
447
|
);
|
|
369
448
|
if (!exists) {
|
|
370
449
|
references.push({ path: referencePath });
|
|
@@ -376,11 +455,14 @@ async function ensureTsReference(workspaceRoot: string, referencePath: string, c
|
|
|
376
455
|
return;
|
|
377
456
|
}
|
|
378
457
|
|
|
379
|
-
await
|
|
458
|
+
await Bun.write(tsconfigPath, updated);
|
|
380
459
|
changes.push(relativeWorkspacePath(workspaceRoot, tsconfigPath));
|
|
381
460
|
}
|
|
382
461
|
|
|
383
|
-
function resolveGithubPagesBasePath(
|
|
462
|
+
function resolveGithubPagesBasePath(
|
|
463
|
+
basePathArg: string | undefined,
|
|
464
|
+
workspaceName: string,
|
|
465
|
+
): string {
|
|
384
466
|
const candidate = (basePathArg ?? workspaceName).trim();
|
|
385
467
|
if (!candidate || candidate === '/') {
|
|
386
468
|
return '/';
|
|
@@ -409,7 +491,10 @@ function ensureLayerIncludes(css: string, layerName: string): string {
|
|
|
409
491
|
return css;
|
|
410
492
|
}
|
|
411
493
|
|
|
412
|
-
const layers = match[1]
|
|
494
|
+
const layers = match[1]
|
|
495
|
+
.split(',')
|
|
496
|
+
.map((layer) => layer.trim())
|
|
497
|
+
.filter(Boolean);
|
|
413
498
|
if (layers.includes(layerName)) {
|
|
414
499
|
return css;
|
|
415
500
|
}
|
|
@@ -417,13 +502,18 @@ function ensureLayerIncludes(css: string, layerName: string): string {
|
|
|
417
502
|
const updated = [...layers];
|
|
418
503
|
const utilitiesIndex = updated.indexOf('utilities');
|
|
419
504
|
const overridesIndex = updated.indexOf('overrides');
|
|
420
|
-
const insertIndex =
|
|
505
|
+
const insertIndex =
|
|
506
|
+
utilitiesIndex >= 0 ? utilitiesIndex : overridesIndex >= 0 ? overridesIndex : updated.length;
|
|
421
507
|
updated.splice(insertIndex, 0, layerName);
|
|
422
508
|
const replacement = `@layer ${updated.join(', ')};`;
|
|
423
509
|
return `${css.slice(0, match.index)}${replacement}${css.slice(match.index + match[0].length)}`;
|
|
424
510
|
}
|
|
425
511
|
|
|
426
|
-
function ensureImportIncludes(
|
|
512
|
+
function ensureImportIncludes(
|
|
513
|
+
css: string,
|
|
514
|
+
importPath: string,
|
|
515
|
+
insertAfterImportPath: string,
|
|
516
|
+
): string {
|
|
427
517
|
if (css.includes(`@import "${importPath}"`) || css.includes(`@import '${importPath}'`)) {
|
|
428
518
|
return css;
|
|
429
519
|
}
|
|
@@ -431,10 +521,8 @@ function ensureImportIncludes(css: string, importPath: string, insertAfterImport
|
|
|
431
521
|
const doubleNeedle = `@import "${insertAfterImportPath}"`;
|
|
432
522
|
const singleNeedle = `@import '${insertAfterImportPath}'`;
|
|
433
523
|
let insertAfterIndex = css.indexOf(doubleNeedle);
|
|
434
|
-
let needle = doubleNeedle;
|
|
435
524
|
if (insertAfterIndex < 0) {
|
|
436
525
|
insertAfterIndex = css.indexOf(singleNeedle);
|
|
437
|
-
needle = singleNeedle;
|
|
438
526
|
}
|
|
439
527
|
|
|
440
528
|
if (insertAfterIndex >= 0) {
|
|
@@ -456,12 +544,16 @@ function ensureImportIncludes(css: string, importPath: string, insertAfterImport
|
|
|
456
544
|
|
|
457
545
|
async function writeTextFile(filePath: string, contents: string, mode?: number): Promise<void> {
|
|
458
546
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
459
|
-
await
|
|
547
|
+
await Bun.write(filePath, contents);
|
|
460
548
|
if (mode !== undefined) {
|
|
461
549
|
await chmod(filePath, mode);
|
|
462
550
|
}
|
|
463
551
|
}
|
|
464
552
|
|
|
553
|
+
async function readTextFile(filePath: string): Promise<string> {
|
|
554
|
+
return await Bun.file(filePath).text();
|
|
555
|
+
}
|
|
556
|
+
|
|
465
557
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
466
558
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
467
559
|
? { ...(value as Record<string, unknown>) }
|
package/src/execute.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface RunCommandOptions {
|
|
|
19
19
|
|
|
20
20
|
export async function runCommand(
|
|
21
21
|
mode: CommandMode,
|
|
22
|
-
options: RunCommandOptions
|
|
22
|
+
options: RunCommandOptions,
|
|
23
23
|
): Promise<CommandExecutionResult> {
|
|
24
24
|
const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
|
|
25
25
|
const providerLoader = options.loadProvider ?? loadProvider;
|
|
@@ -37,6 +37,7 @@ export async function runCommand(
|
|
|
37
37
|
env: createWorkspaceRuntimeEnv(workspace.root, mode, options.env),
|
|
38
38
|
incremental: false,
|
|
39
39
|
});
|
|
40
|
+
assertNoFatalDiagnostics(kind, mode, result);
|
|
40
41
|
|
|
41
42
|
targets.push({
|
|
42
43
|
kind,
|
|
@@ -57,25 +58,26 @@ async function prepareCommandTarget(
|
|
|
57
58
|
workspaceRoot: string,
|
|
58
59
|
kind: BuildTargetKind,
|
|
59
60
|
mode: CommandMode,
|
|
60
|
-
env?: Record<string, string | undefined
|
|
61
|
+
env?: Record<string, string | undefined>,
|
|
61
62
|
): Promise<void> {
|
|
62
63
|
if (kind !== 'frontend' || mode !== 'publish') {
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
// Frontend publish consumes build/frontend artifacts while generating dist output.
|
|
67
|
-
await provider.build({
|
|
68
|
+
const result = await provider.build({
|
|
68
69
|
workspaceRoot,
|
|
69
70
|
env: createWorkspaceRuntimeEnv(workspaceRoot, 'build', env),
|
|
70
71
|
incremental: false,
|
|
71
72
|
});
|
|
73
|
+
assertNoFatalDiagnostics(kind, 'prebuild', result);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
function resolveOutputRoot(
|
|
75
77
|
workspaceRoot: string,
|
|
76
78
|
kind: BuildTargetKind,
|
|
77
79
|
mode: CommandMode,
|
|
78
|
-
buildRoot: string
|
|
80
|
+
buildRoot: string,
|
|
79
81
|
): string {
|
|
80
82
|
if (kind === 'frontend' && mode === 'publish') {
|
|
81
83
|
return path.join(workspaceRoot, 'dist', 'frontend');
|
|
@@ -83,3 +85,25 @@ function resolveOutputRoot(
|
|
|
83
85
|
|
|
84
86
|
return buildRoot;
|
|
85
87
|
}
|
|
88
|
+
|
|
89
|
+
function assertNoFatalDiagnostics(
|
|
90
|
+
kind: BuildTargetKind,
|
|
91
|
+
phase: CommandMode | 'prebuild',
|
|
92
|
+
result: CommandExecutionResult['targets'][number]['result'],
|
|
93
|
+
): void {
|
|
94
|
+
const errors = result.manifest.diagnostics.filter(
|
|
95
|
+
(diagnostic) => diagnostic.severity === 'error',
|
|
96
|
+
);
|
|
97
|
+
if (errors.length === 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const summary = errors
|
|
102
|
+
.slice(0, 3)
|
|
103
|
+
.map((diagnostic) => diagnostic.message)
|
|
104
|
+
.join(' | ');
|
|
105
|
+
const extra = errors.length > 3 ? ` (+${errors.length - 3} more)` : '';
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${kind} ${phase} reported ${errors.length} error diagnostic(s): ${summary}${extra}`,
|
|
108
|
+
);
|
|
109
|
+
}
|