@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.
Files changed (77) hide show
  1. package/README.md +13 -0
  2. package/assets/deployment/docker/.dockerignore +7 -0
  3. package/assets/deployment/docker/Dockerfile +17 -0
  4. package/assets/deployment/docker/README.md +44 -0
  5. package/assets/deployment/docker/example.env +3 -0
  6. package/assets/features/client_nav/client_nav.ts +369 -264
  7. package/assets/features/client_nav/document_navigation.ts +344 -0
  8. package/assets/features/client_nav/form_enhancement.ts +275 -0
  9. package/assets/templates/api/src/backend/index.ts +71 -10
  10. package/assets/templates/api/src/backend/tsconfig.json +6 -1
  11. package/assets/templates/full/src/backend/index.ts +71 -10
  12. package/assets/templates/full/src/backend/module.ts +515 -0
  13. package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
  14. package/assets/templates/full/src/backend/tsconfig.json +6 -1
  15. package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
  16. package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
  17. package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
  18. package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
  19. package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
  20. package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
  21. package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
  22. package/package.json +31 -13
  23. package/scripts/check-feature-projections.mjs +87 -0
  24. package/scripts/check-full-demo-sync.mjs +89 -0
  25. package/scripts/check-package-install.mjs +537 -0
  26. package/scripts/check-standalone-install.mjs +221 -0
  27. package/scripts/pack-standalone.mjs +52 -28
  28. package/scripts/publish.sh +9 -0
  29. package/scripts/run-tests.mjs +99 -0
  30. package/scripts/sync-assets.mjs +175 -17
  31. package/src/add-backend-compat.ts +628 -0
  32. package/src/add-backend.ts +155 -27
  33. package/src/add.ts +111 -4
  34. package/src/agent.ts +393 -0
  35. package/src/api-watch.ts +7 -4
  36. package/src/backend-inspect.ts +70 -2
  37. package/src/backend-runtime.ts +22 -14
  38. package/src/build.ts +1 -3
  39. package/src/bun-generated-frontend-watch.ts +209 -0
  40. package/src/bun-globals.d.ts +23 -0
  41. package/src/bun-spa-document.ts +310 -0
  42. package/src/bun-spa-routes.ts +159 -0
  43. package/src/bun-spa-watch.ts +29 -0
  44. package/src/bun-ssg-watch.ts +304 -0
  45. package/src/cli.ts +381 -50
  46. package/src/compile-tests.ts +37 -29
  47. package/src/dev-server.ts +214 -143
  48. package/src/doctor.ts +164 -0
  49. package/src/enable-assets.ts +18 -1
  50. package/src/enable.ts +133 -41
  51. package/src/execute.ts +28 -4
  52. package/src/external-workspace.ts +178 -0
  53. package/src/format.ts +296 -17
  54. package/src/frontend-inspect.ts +32 -0
  55. package/src/frontend-watch.ts +27 -102
  56. package/src/full-watch.ts +13 -18
  57. package/src/index.ts +7 -0
  58. package/src/init-assets.ts +41 -11
  59. package/src/init.ts +85 -71
  60. package/src/inspect.ts +112 -0
  61. package/src/mcp/run-cli-json.ts +46 -0
  62. package/src/mcp/server.ts +307 -0
  63. package/src/operations.ts +176 -0
  64. package/src/providers.ts +20 -18
  65. package/src/refresh.ts +29 -3
  66. package/src/repair.ts +110 -43
  67. package/src/runtime-filter.ts +41 -0
  68. package/src/runtime.ts +1 -1
  69. package/src/smoke.ts +48 -16
  70. package/src/test.ts +54 -16
  71. package/src/testing-runtime.ts +273 -0
  72. package/src/types.ts +1 -4
  73. package/src/watch-events.ts +46 -17
  74. package/src/watch.ts +5 -1
  75. package/src/workspace-watcher.ts +10 -6
  76. package/src/workspace.ts +4 -2
  77. 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
+ }
@@ -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.5
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, copyFile, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
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(workspace.root, path.basename(workspace.root), rest[0], false, changes);
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(workspace.root, path.basename(workspace.root), rest[0], true, changes);
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(workspaceRoot: string, args: readonly string[], changes: string[]): Promise<void> {
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 copyFile(asset.sourcePath, targetPath);
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 copyFile(asset.sourcePath, targetPath);
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(workspaceRoot, { enableBackend: true, mode: 'full' }, changes);
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(workspaceRoot, { enableGithubPages: true, ensureDeployScript: true }, changes);
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 readFile(appTsPath, 'utf8');
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 writeFile(appTsPath, updated, 'utf8');
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 readFile(appCssPath, 'utf8');
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 writeFile(appCssPath, updated, 'utf8');
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 readFile(appHtmlPath, 'utf8');
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 writeFile(appHtmlPath, updated, 'utf8');
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 readFile(packageJsonPath, 'utf8');
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 writeFile(packageJsonPath, updated, 'utf8');
355
+ await Bun.write(packageJsonPath, updated);
326
356
  changes.push(relativeWorkspacePath(workspaceRoot, packageJsonPath));
327
357
  }
328
358
 
329
- async function updateFrontendConfig(workspaceRoot: string, basePath: string, changes: string[]): Promise<void> {
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 readFile(configPath, 'utf8')) as Record<string, unknown>;
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 readFile(configPath, 'utf8') : null;
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(workspaceRoot: string, referencePath: string, changes: string[]): Promise<void> {
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 readFile(tsconfigPath, 'utf8');
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((entry) =>
365
- typeof entry === 'object'
366
- && entry !== null
367
- && (entry as Record<string, unknown>).path === referencePath
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 writeFile(tsconfigPath, updated, 'utf8');
458
+ await Bun.write(tsconfigPath, updated);
380
459
  changes.push(relativeWorkspacePath(workspaceRoot, tsconfigPath));
381
460
  }
382
461
 
383
- function resolveGithubPagesBasePath(basePathArg: string | undefined, workspaceName: string): string {
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].split(',').map((layer) => layer.trim()).filter(Boolean);
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 = utilitiesIndex >= 0 ? utilitiesIndex : overridesIndex >= 0 ? overridesIndex : updated.length;
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(css: string, importPath: string, insertAfterImportPath: string): string {
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 writeFile(filePath, contents, 'utf8');
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
+ }