@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.
Files changed (123) hide show
  1. package/README.md +69 -0
  2. package/assets/features/client_nav/client_nav.ts +469 -0
  3. package/assets/features/content_nav/content_nav.css +170 -0
  4. package/assets/features/content_nav/content_nav.ts +358 -0
  5. package/assets/features/router/router-types.ts +6 -0
  6. package/assets/features/router/router.ts +118 -0
  7. package/assets/features/search/search.css +204 -0
  8. package/assets/features/search/search.ts +627 -0
  9. package/assets/templates/api/src/backend/index.ts +13 -0
  10. package/assets/templates/api/src/backend/tsconfig.json +15 -0
  11. package/assets/templates/api/src/shared/router-types.ts +23 -0
  12. package/assets/templates/api/src/shared/tsconfig.json +10 -0
  13. package/assets/templates/api/src/shared/types/index.ts +4 -0
  14. package/assets/templates/full/src/backend/index.ts +13 -0
  15. package/assets/templates/full/src/backend/tsconfig.json +15 -0
  16. package/assets/templates/full/src/frontend/app/app.css +65 -0
  17. package/assets/templates/full/src/frontend/app/app.html +13 -0
  18. package/assets/templates/full/src/frontend/app/app.ts +188 -0
  19. package/assets/templates/full/src/frontend/app/error.ts +127 -0
  20. package/assets/templates/full/src/frontend/app/hmr.js +355 -0
  21. package/assets/templates/full/src/frontend/app/navigation.ts +8 -0
  22. package/assets/templates/full/src/frontend/app/refresh.js +114 -0
  23. package/assets/templates/full/src/frontend/app/router.ts +126 -0
  24. package/assets/templates/full/src/frontend/app/styles/base.css +2 -0
  25. package/assets/templates/full/src/frontend/app/styles/reset.css +48 -0
  26. package/assets/templates/full/src/frontend/pages/home/index.css +21 -0
  27. package/assets/templates/full/src/frontend/pages/home/index.html +10 -0
  28. package/assets/templates/full/src/frontend/pages/home/index.ts +18 -0
  29. package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +21 -0
  30. package/assets/templates/full/src/frontend/tsconfig.json +20 -0
  31. package/assets/templates/full/src/shared/router-types.ts +23 -0
  32. package/assets/templates/full/src/shared/tsconfig.json +10 -0
  33. package/assets/templates/full/src/shared/types/index.ts +4 -0
  34. package/assets/templates/shared/Errors.404.html +23 -0
  35. package/assets/templates/shared/Errors.500.html +23 -0
  36. package/assets/templates/shared/Errors.default.html +23 -0
  37. package/assets/templates/shared/types/global.d.ts +32 -0
  38. package/assets/templates/shared/types.global.d.ts +32 -0
  39. package/assets/templates/spa/src/frontend/app/app.css +65 -0
  40. package/assets/templates/spa/src/frontend/app/app.html +13 -0
  41. package/assets/templates/spa/src/frontend/app/app.ts +188 -0
  42. package/assets/templates/spa/src/frontend/app/error.ts +127 -0
  43. package/assets/templates/spa/src/frontend/app/hmr.js +355 -0
  44. package/assets/templates/spa/src/frontend/app/navigation.ts +8 -0
  45. package/assets/templates/spa/src/frontend/app/refresh.js +114 -0
  46. package/assets/templates/spa/src/frontend/app/router.ts +126 -0
  47. package/assets/templates/spa/src/frontend/app/styles/base.css +2 -0
  48. package/assets/templates/spa/src/frontend/app/styles/reset.css +48 -0
  49. package/assets/templates/spa/src/frontend/pages/home/index.css +21 -0
  50. package/assets/templates/spa/src/frontend/pages/home/index.html +10 -0
  51. package/assets/templates/spa/src/frontend/pages/home/index.ts +18 -0
  52. package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +21 -0
  53. package/assets/templates/spa/src/frontend/tsconfig.json +20 -0
  54. package/assets/templates/spa/src/shared/router-types.ts +23 -0
  55. package/assets/templates/spa/src/shared/tsconfig.json +10 -0
  56. package/assets/templates/spa/src/shared/types/index.ts +4 -0
  57. package/assets/templates/ssg/src/frontend/app/app.css +12 -0
  58. package/assets/templates/ssg/src/frontend/app/app.html +43 -0
  59. package/assets/templates/ssg/src/frontend/app/app.ts +190 -0
  60. package/assets/templates/ssg/src/frontend/app/error.ts +127 -0
  61. package/assets/templates/ssg/src/frontend/app/hmr.js +370 -0
  62. package/assets/templates/ssg/src/frontend/app/refresh.js +163 -0
  63. package/assets/templates/ssg/src/frontend/app/scripts/components/drawer.ts +183 -0
  64. package/assets/templates/ssg/src/frontend/app/scripts/components/menu.ts +75 -0
  65. package/assets/templates/ssg/src/frontend/app/styles/base.css +77 -0
  66. package/assets/templates/ssg/src/frontend/app/styles/components/buttons.css +108 -0
  67. package/assets/templates/ssg/src/frontend/app/styles/components/drawer.css +12 -0
  68. package/assets/templates/ssg/src/frontend/app/styles/components/header.css +164 -0
  69. package/assets/templates/ssg/src/frontend/app/styles/components/markdown.css +25 -0
  70. package/assets/templates/ssg/src/frontend/app/styles/layout.css +41 -0
  71. package/assets/templates/ssg/src/frontend/app/styles/reset.css +56 -0
  72. package/assets/templates/ssg/src/frontend/app/styles/tokens.css +72 -0
  73. package/assets/templates/ssg/src/frontend/app/styles/utilities.css +14 -0
  74. package/assets/templates/ssg/src/frontend/content/_sidebar.json +14 -0
  75. package/assets/templates/ssg/src/frontend/content/content-pipeline.md +82 -0
  76. package/assets/templates/ssg/src/frontend/content/css-playbook.md +68 -0
  77. package/assets/templates/ssg/src/frontend/content/hosting.md +48 -0
  78. package/assets/templates/ssg/src/frontend/pages/about/index.css +33 -0
  79. package/assets/templates/ssg/src/frontend/pages/about/index.html +60 -0
  80. package/assets/templates/ssg/src/frontend/pages/docs/index.css +505 -0
  81. package/assets/templates/ssg/src/frontend/pages/docs/index.html +52 -0
  82. package/assets/templates/ssg/src/frontend/pages/docs/index.ts +495 -0
  83. package/assets/templates/ssg/src/frontend/pages/home/index.css +91 -0
  84. package/assets/templates/ssg/src/frontend/pages/home/index.html +38 -0
  85. package/assets/templates/ssg/src/frontend/pages/home/tests/home.test.ts +24 -0
  86. package/assets/templates/ssg/src/frontend/tsconfig.json +13 -0
  87. package/package.json +41 -0
  88. package/scripts/pack-standalone.mjs +127 -0
  89. package/scripts/sync-assets.mjs +87 -0
  90. package/src/add-backend.ts +164 -0
  91. package/src/add.ts +112 -0
  92. package/src/api-watch.ts +84 -0
  93. package/src/backend-inspect.ts +45 -0
  94. package/src/backend-runtime.ts +286 -0
  95. package/src/build-plan.ts +12 -0
  96. package/src/build.ts +10 -0
  97. package/src/cli.ts +569 -0
  98. package/src/compile-tests.ts +61 -0
  99. package/src/dev-server.ts +393 -0
  100. package/src/enable-assets.ts +196 -0
  101. package/src/enable.ts +477 -0
  102. package/src/execute.ts +85 -0
  103. package/src/format.ts +254 -0
  104. package/src/frontend-watch.ts +145 -0
  105. package/src/full-watch.ts +80 -0
  106. package/src/index.ts +20 -0
  107. package/src/init-assets.ts +96 -0
  108. package/src/init.ts +339 -0
  109. package/src/paths.ts +26 -0
  110. package/src/providers.ts +88 -0
  111. package/src/publish.ts +8 -0
  112. package/src/refresh.ts +56 -0
  113. package/src/repair.ts +414 -0
  114. package/src/runtime.ts +48 -0
  115. package/src/smoke.ts +161 -0
  116. package/src/stop-signal.ts +26 -0
  117. package/src/test.ts +215 -0
  118. package/src/types.ts +29 -0
  119. package/src/watch-daemon-client.ts +171 -0
  120. package/src/watch-events.ts +195 -0
  121. package/src/watch.ts +66 -0
  122. package/src/workspace-watcher.ts +251 -0
  123. package/src/workspace.ts +55 -0
package/src/format.ts ADDED
@@ -0,0 +1,254 @@
1
+ import type { EnableResult } from './enable.ts';
2
+ import type { InitResult } from './init.ts';
3
+ import type { RefreshResult } from './refresh.ts';
4
+ import type { RepairResult } from './repair.ts';
5
+ import type { BackendInspectResult } from './backend-inspect.ts';
6
+ import type { SmokeResult } from './smoke.ts';
7
+ import type { TestCommandResult } from './test.ts';
8
+ import { formatFailedTests } from './test.ts';
9
+ import type { CommandExecutionResult } from './types.ts';
10
+
11
+ export function formatBuildSummary(result: CommandExecutionResult): string {
12
+ return formatExecutionSummary(result);
13
+ }
14
+
15
+ export function formatPublishSummary(result: CommandExecutionResult): string {
16
+ return formatExecutionSummary(result);
17
+ }
18
+
19
+ export function formatEnableSummary(result: EnableResult): string {
20
+ const lines = [
21
+ '[webstir] enable complete',
22
+ `feature: ${result.feature}`,
23
+ `root: ${result.workspaceRoot}`,
24
+ ];
25
+
26
+ if (result.changes.length === 0) {
27
+ lines.push('changes: none');
28
+ return lines.join('\n');
29
+ }
30
+
31
+ lines.push(`changes: ${result.changes.length}`);
32
+ for (const change of result.changes) {
33
+ lines.push(` - ${change}`);
34
+ }
35
+
36
+ return lines.join('\n');
37
+ }
38
+
39
+ export function formatInitSummary(result: InitResult): string {
40
+ return formatWorkspaceMutationSummary('[webstir] init complete', result.mode, result.workspaceRoot, result.changes);
41
+ }
42
+
43
+ export function formatRefreshSummary(result: RefreshResult): string {
44
+ return formatWorkspaceMutationSummary('[webstir] refresh complete', result.mode, result.workspaceRoot, result.changes);
45
+ }
46
+
47
+ export function formatRepairSummary(result: RepairResult): string {
48
+ const lines = [
49
+ '[webstir] repair complete',
50
+ `mode: ${result.mode}`,
51
+ `root: ${result.workspaceRoot}`,
52
+ `dry-run: ${result.dryRun ? 'true' : 'false'}`,
53
+ ];
54
+
55
+ if (result.changes.length === 0) {
56
+ lines.push('changes: none');
57
+ return lines.join('\n');
58
+ }
59
+
60
+ lines.push(`changes: ${result.changes.length}`);
61
+ for (const change of result.changes) {
62
+ lines.push(` - ${change}`);
63
+ }
64
+
65
+ return lines.join('\n');
66
+ }
67
+
68
+ export function formatBackendInspectSummary(result: BackendInspectResult): string {
69
+ const lines = [
70
+ '[webstir] backend-inspect complete',
71
+ `workspace: ${result.workspace.name}`,
72
+ `mode: ${result.workspace.mode}`,
73
+ `root: ${result.workspace.root}`,
74
+ `build: ${result.buildRoot}`,
75
+ `module: ${result.manifest.name}@${result.manifest.version}`,
76
+ `capabilities: ${result.manifest.capabilities && result.manifest.capabilities.length > 0 ? result.manifest.capabilities.join(', ') : 'none'}`,
77
+ ];
78
+
79
+ const routes = result.manifest.routes ?? [];
80
+ lines.push(`routes: ${routes.length}`);
81
+ for (const route of routes) {
82
+ lines.push(` - ${route.method} ${route.path}${route.name ? ` (${route.name})` : ''}`);
83
+ }
84
+
85
+ const jobs = result.manifest.jobs ?? [];
86
+ lines.push(`jobs: ${jobs.length}`);
87
+ for (const job of jobs) {
88
+ const details = [
89
+ job.schedule ? `schedule: ${job.schedule}` : undefined,
90
+ 'description' in job && typeof (job as { description?: unknown }).description === 'string'
91
+ ? `description: ${(job as { description?: string }).description}`
92
+ : undefined,
93
+ job.priority !== undefined ? `priority: ${String(job.priority)}` : undefined,
94
+ ].filter(Boolean);
95
+ lines.push(` - ${job.name}${details.length > 0 ? ` (${details.join(', ')})` : ''}`);
96
+ }
97
+
98
+ return lines.join('\n');
99
+ }
100
+
101
+ export function formatTestSummary(result: TestCommandResult): string {
102
+ const lines = [
103
+ '[webstir] test complete',
104
+ `workspace: ${result.workspace.name}`,
105
+ `mode: ${result.workspace.mode}`,
106
+ `root: ${result.workspace.root}`,
107
+ `runtime: ${result.runtime}`,
108
+ `build-targets: ${result.builtTargets.length > 0 ? result.builtTargets.join(', ') : 'none'}`,
109
+ ];
110
+
111
+ if (result.filterMessage) {
112
+ lines.push(`filter: ${result.filterMessage}`);
113
+ }
114
+
115
+ lines.push(`tests: ${result.summary.total}`);
116
+ lines.push(`passed: ${result.summary.passed}`);
117
+ lines.push(`failed: ${result.summary.failed}`);
118
+ lines.push(`durationMs: ${result.summary.durationMs}`);
119
+
120
+ const failures = formatFailedTests(result.summary.results);
121
+ if (failures.length > 0) {
122
+ lines.push(`failures: ${failures.length}`);
123
+ for (const failure of failures) {
124
+ lines.push(` - ${failure}`);
125
+ }
126
+ }
127
+
128
+ return lines.join('\n');
129
+ }
130
+
131
+ export function formatSmokeSummary(result: SmokeResult): string {
132
+ const lines = [
133
+ '[webstir] smoke complete',
134
+ `workspace: ${result.workspace.name}`,
135
+ `mode: ${result.workspace.mode}`,
136
+ `root: ${result.workspace.root}`,
137
+ `workspace-source: ${result.usedTempWorkspace ? 'temporary copy' : 'explicit workspace'}`,
138
+ ];
139
+
140
+ if (result.source) {
141
+ lines.push(`source: ${result.source}`);
142
+ }
143
+
144
+ lines.push(`phases: ${result.phases.length}`);
145
+ for (const phase of result.phases) {
146
+ lines.push(` - ${phase.name}: ${phase.detail}`);
147
+ }
148
+
149
+ return lines.join('\n');
150
+ }
151
+
152
+ export function formatAddSummary(
153
+ header: string,
154
+ target: string,
155
+ workspaceRoot: string,
156
+ changes: readonly string[],
157
+ note?: string
158
+ ): string {
159
+ const lines = [
160
+ header,
161
+ `target: ${target}`,
162
+ `root: ${workspaceRoot}`,
163
+ ];
164
+
165
+ if (changes.length === 0) {
166
+ lines.push('changes: none');
167
+ if (note) {
168
+ lines.push(`note: ${note}`);
169
+ }
170
+ return lines.join('\n');
171
+ }
172
+
173
+ lines.push(`changes: ${changes.length}`);
174
+ for (const change of changes) {
175
+ lines.push(` - ${change}`);
176
+ }
177
+
178
+ if (note) {
179
+ lines.push(`note: ${note}`);
180
+ }
181
+
182
+ return lines.join('\n');
183
+ }
184
+
185
+ function formatExecutionSummary(result: CommandExecutionResult): string {
186
+ const lines = [
187
+ `[webstir] ${result.mode} complete`,
188
+ `workspace: ${result.workspace.name}`,
189
+ `mode: ${result.workspace.mode}`,
190
+ `root: ${result.workspace.root}`,
191
+ ];
192
+
193
+ for (const target of result.targets) {
194
+ const diagnostics = summarizeDiagnostics(target.result);
195
+ lines.push(
196
+ `${target.kind}: ${target.result.artifacts.length} artifacts, ` +
197
+ `${target.result.manifest.entryPoints.length} entries, ` +
198
+ `${target.result.manifest.staticAssets.length} static assets -> ${target.outputRoot}`
199
+ );
200
+
201
+ if (diagnostics.errors > 0 || diagnostics.warnings > 0) {
202
+ lines.push(
203
+ `${target.kind}: ${diagnostics.errors} error(s), ${diagnostics.warnings} warning(s), ${diagnostics.info} info`
204
+ );
205
+ }
206
+ }
207
+
208
+ return lines.join('\n');
209
+ }
210
+
211
+ function formatWorkspaceMutationSummary(
212
+ header: string,
213
+ mode: string,
214
+ workspaceRoot: string,
215
+ changes: readonly string[]
216
+ ): string {
217
+ const lines = [
218
+ header,
219
+ `mode: ${mode}`,
220
+ `root: ${workspaceRoot}`,
221
+ ];
222
+
223
+ if (changes.length === 0) {
224
+ lines.push('changes: none');
225
+ return lines.join('\n');
226
+ }
227
+
228
+ lines.push(`changes: ${changes.length}`);
229
+ for (const change of changes) {
230
+ lines.push(` - ${change}`);
231
+ }
232
+
233
+ return lines.join('\n');
234
+ }
235
+
236
+ function summarizeDiagnostics(result: CommandExecutionResult['targets'][number]['result']) {
237
+ const counts = {
238
+ errors: 0,
239
+ warnings: 0,
240
+ info: 0,
241
+ };
242
+
243
+ for (const diagnostic of result.manifest.diagnostics) {
244
+ if (diagnostic.severity === 'error') {
245
+ counts.errors += 1;
246
+ } else if (diagnostic.severity === 'warn') {
247
+ counts.warnings += 1;
248
+ } else {
249
+ counts.info += 1;
250
+ }
251
+ }
252
+
253
+ return counts;
254
+ }
@@ -0,0 +1,145 @@
1
+ import path from 'node:path';
2
+
3
+ import { DevServer, type DevServerAddress } from './dev-server.ts';
4
+ import { createStopSignal } from './stop-signal.ts';
5
+ import { FrontendWatchDaemonClient } from './watch-daemon-client.ts';
6
+ import { collectWatchActions, type StructuredDiagnosticPayload } from './watch-events.ts';
7
+ import type { WorkspaceDescriptor } from './types.ts';
8
+ import type { WatchIo, WatchOptions } from './watch.ts';
9
+ import { WorkspaceWatcher, type WorkspaceWatchEvent } from './workspace-watcher.ts';
10
+
11
+ export interface FrontendWatchSession {
12
+ readonly address: DevServerAddress;
13
+ waitForExit(): Promise<number | null>;
14
+ stop(): Promise<void>;
15
+ }
16
+
17
+ interface FrontendWatchSessionOptions extends WatchOptions {
18
+ readonly server?: DevServer;
19
+ }
20
+
21
+ export async function runFrontendWatch(
22
+ workspace: WorkspaceDescriptor,
23
+ options: WatchOptions,
24
+ io: WatchIo
25
+ ): Promise<void> {
26
+ const session = await startFrontendWatchSession(workspace, options, io);
27
+
28
+ io.stdout.write(
29
+ `[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${session.address.origin}\n`
30
+ );
31
+
32
+ const stopSignal = createStopSignal();
33
+
34
+ try {
35
+ const daemonExitCode = await Promise.race([
36
+ session.waitForExit(),
37
+ stopSignal.promise.then(() => null),
38
+ ]);
39
+
40
+ if (typeof daemonExitCode === 'number' && daemonExitCode !== 0) {
41
+ throw new Error(`Frontend watch daemon exited with code ${daemonExitCode}.`);
42
+ }
43
+ } finally {
44
+ stopSignal.dispose();
45
+ await session.stop();
46
+ }
47
+ }
48
+
49
+ export async function startFrontendWatchSession(
50
+ workspace: WorkspaceDescriptor,
51
+ options: FrontendWatchSessionOptions,
52
+ io: WatchIo
53
+ ): Promise<FrontendWatchSession> {
54
+ const server = options.server ?? new DevServer({
55
+ buildRoot: path.join(workspace.root, 'build', 'frontend'),
56
+ host: options.host,
57
+ port: options.port,
58
+ });
59
+ const ownsServer = options.server === undefined;
60
+ const address = await server.start();
61
+
62
+ let initialBuildReady = false;
63
+ const daemon = new FrontendWatchDaemonClient({
64
+ workspaceRoot: workspace.root,
65
+ verbose: options.verbose,
66
+ hmrVerbose: options.hmrVerbose,
67
+ env: options.env,
68
+ onLine(line) {
69
+ io.stdout.write(`${line}\n`);
70
+ },
71
+ onErrorLine(line) {
72
+ io.stderr.write(`${line}\n`);
73
+ },
74
+ onDiagnostic(payload) {
75
+ if (!initialBuildReady && payload.code === 'frontend.watch.pipeline.success') {
76
+ initialBuildReady = true;
77
+ io.stdout.write(`[webstir] frontend ready at ${address.origin}\n`);
78
+ }
79
+
80
+ void applyDiagnostic(payload, server);
81
+ },
82
+ });
83
+
84
+ const watcher = new WorkspaceWatcher({
85
+ workspaceRoot: workspace.root,
86
+ onEvent(event) {
87
+ void dispatchWorkspaceEvent(event, daemon);
88
+ },
89
+ });
90
+
91
+ await daemon.start();
92
+ await watcher.start();
93
+ await daemon.sendStart();
94
+
95
+ let stopPromise: Promise<void> | null = null;
96
+
97
+ return {
98
+ address,
99
+ async waitForExit() {
100
+ return await daemon.waitForExit();
101
+ },
102
+ async stop() {
103
+ stopPromise ??= (async () => {
104
+ await watcher.stop();
105
+ await daemon.stop();
106
+ if (ownsServer) {
107
+ await server.stop();
108
+ }
109
+ })();
110
+
111
+ await stopPromise;
112
+ },
113
+ };
114
+ }
115
+
116
+ async function applyDiagnostic(payload: StructuredDiagnosticPayload, server: DevServer): Promise<void> {
117
+ const actions = collectWatchActions(payload);
118
+ for (const action of actions) {
119
+ switch (action.type) {
120
+ case 'status':
121
+ await server.publishStatus(action.status);
122
+ break;
123
+ case 'hmr':
124
+ await server.publishHotUpdate(action.payload);
125
+ break;
126
+ case 'reload':
127
+ await server.publishReload();
128
+ break;
129
+ default:
130
+ break;
131
+ }
132
+ }
133
+ }
134
+
135
+ async function dispatchWorkspaceEvent(
136
+ event: WorkspaceWatchEvent,
137
+ daemon: FrontendWatchDaemonClient
138
+ ): Promise<void> {
139
+ if (event.type === 'reload') {
140
+ await daemon.sendReload();
141
+ return;
142
+ }
143
+
144
+ await daemon.sendChange(event.path);
145
+ }
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { createServer } from 'node:net';
3
+
4
+ import { startApiWatchSession } from './api-watch.ts';
5
+ import { DevServer } from './dev-server.ts';
6
+ import { startFrontendWatchSession } from './frontend-watch.ts';
7
+ import { createStopSignal } from './stop-signal.ts';
8
+ import type { WorkspaceDescriptor } from './types.ts';
9
+ import type { WatchIo, WatchOptions } from './watch.ts';
10
+
11
+ export async function runFullWatch(
12
+ workspace: WorkspaceDescriptor,
13
+ options: WatchOptions,
14
+ io: WatchIo
15
+ ): Promise<void> {
16
+ const backendPort = await allocateBackendPort();
17
+ const apiSession = await startApiWatchSession(workspace, { ...options, port: backendPort }, io);
18
+ const server = new DevServer({
19
+ buildRoot: path.join(workspace.root, 'build', 'frontend'),
20
+ host: options.host,
21
+ port: options.port,
22
+ apiProxyOrigin: apiSession.origin,
23
+ });
24
+
25
+ let frontendSession: Awaited<ReturnType<typeof startFrontendWatchSession>> | undefined;
26
+
27
+ try {
28
+ frontendSession = await startFrontendWatchSession(workspace, { ...options, server }, io);
29
+ io.stdout.write(
30
+ `[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${frontendSession.address.origin}\napi: ${apiSession.origin}\n`
31
+ );
32
+
33
+ const stopSignal = createStopSignal();
34
+ try {
35
+ const daemonExitCode = await Promise.race([
36
+ frontendSession.waitForExit(),
37
+ stopSignal.promise.then(() => null),
38
+ ]);
39
+
40
+ if (typeof daemonExitCode === 'number' && daemonExitCode !== 0) {
41
+ throw new Error(`Frontend watch daemon exited with code ${daemonExitCode}.`);
42
+ }
43
+ } finally {
44
+ stopSignal.dispose();
45
+ }
46
+ } finally {
47
+ if (frontendSession) {
48
+ await frontendSession.stop();
49
+ }
50
+ await server.stop();
51
+ await apiSession.stop();
52
+ }
53
+ }
54
+
55
+ async function allocateBackendPort(): Promise<number> {
56
+ const server = createServer();
57
+ await new Promise<void>((resolve, reject) => {
58
+ server.once('error', reject);
59
+ server.listen(0, '127.0.0.1', () => resolve());
60
+ });
61
+
62
+ const address = server.address();
63
+ if (!address || typeof address === 'string') {
64
+ throw new Error('Failed to allocate a backend runtime port.');
65
+ }
66
+
67
+ const port = address.port;
68
+ await new Promise<void>((resolve, reject) => {
69
+ server.close((error) => {
70
+ if (error) {
71
+ reject(error);
72
+ return;
73
+ }
74
+
75
+ resolve();
76
+ });
77
+ });
78
+
79
+ return port;
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export * from './add.ts';
2
+ export * from './add-backend.ts';
3
+ export * from './backend-inspect.ts';
4
+ export * from './build-plan.ts';
5
+ export * from './build.ts';
6
+ export * from './dev-server.ts';
7
+ export * from './enable.ts';
8
+ export * from './execute.ts';
9
+ export * from './format.ts';
10
+ export * from './init.ts';
11
+ export * from './publish.ts';
12
+ export * from './repair.ts';
13
+ export * from './refresh.ts';
14
+ export * from './runtime.ts';
15
+ export * from './smoke.ts';
16
+ export * from './test.ts';
17
+ export * from './types.ts';
18
+ export * from './watch-events.ts';
19
+ export * from './watch.ts';
20
+ export * from './workspace.ts';
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import { readdir } from 'node:fs/promises';
3
+
4
+ import { assetsRoot } from './paths.ts';
5
+ import type { WorkspaceMode } from './types.ts';
6
+
7
+ export interface ScaffoldAsset {
8
+ readonly sourcePath: string;
9
+ readonly targetPath: string;
10
+ }
11
+
12
+ const templatesRoot = path.join(assetsRoot, 'templates');
13
+ const sharedTemplateRoot = path.join(templatesRoot, 'shared');
14
+ const ssgTemplateRoot = path.join(templatesRoot, 'ssg');
15
+ const spaTemplateRoot = path.join(templatesRoot, 'spa');
16
+ const apiTemplateRoot = path.join(templatesRoot, 'api');
17
+ const fullTemplateRoot = path.join(templatesRoot, 'full');
18
+
19
+ export function getRootScaffoldAssets(): readonly ScaffoldAsset[] {
20
+ return [
21
+ createAsset(sharedTemplateRoot, 'Errors.404.html', 'Errors.404.html'),
22
+ createAsset(sharedTemplateRoot, 'Errors.500.html', 'Errors.500.html'),
23
+ createAsset(sharedTemplateRoot, 'Errors.default.html', 'Errors.default.html'),
24
+ createAsset(sharedTemplateRoot, 'types.global.d.ts', 'types.global.d.ts'),
25
+ createAsset(sharedTemplateRoot, path.join('types', 'global.d.ts'), path.join('types', 'global.d.ts')),
26
+ ];
27
+ }
28
+
29
+ export async function getModeScaffoldAssets(mode: WorkspaceMode): Promise<readonly ScaffoldAsset[]> {
30
+ switch (mode) {
31
+ case 'ssg':
32
+ return collectModeAssets([
33
+ { sourceRoot: path.join(ssgTemplateRoot, 'src', 'frontend'), targetRoot: path.join('src', 'frontend') },
34
+ ]);
35
+ case 'spa':
36
+ return collectModeAssets([
37
+ { sourceRoot: path.join(spaTemplateRoot, 'src', 'frontend'), targetRoot: path.join('src', 'frontend') },
38
+ { sourceRoot: path.join(spaTemplateRoot, 'src', 'shared'), targetRoot: path.join('src', 'shared') },
39
+ ]);
40
+ case 'api':
41
+ return collectModeAssets([
42
+ { sourceRoot: path.join(apiTemplateRoot, 'src', 'backend'), targetRoot: path.join('src', 'backend') },
43
+ { sourceRoot: path.join(apiTemplateRoot, 'src', 'shared'), targetRoot: path.join('src', 'shared') },
44
+ ]);
45
+ case 'full':
46
+ return collectModeAssets([
47
+ { sourceRoot: path.join(fullTemplateRoot, 'src', 'frontend'), targetRoot: path.join('src', 'frontend') },
48
+ { sourceRoot: path.join(fullTemplateRoot, 'src', 'backend'), targetRoot: path.join('src', 'backend') },
49
+ { sourceRoot: path.join(fullTemplateRoot, 'src', 'shared'), targetRoot: path.join('src', 'shared') },
50
+ ]);
51
+ }
52
+ }
53
+
54
+ async function collectModeAssets(
55
+ roots: readonly { sourceRoot: string; targetRoot: string }[]
56
+ ): Promise<readonly ScaffoldAsset[]> {
57
+ const assets: ScaffoldAsset[] = [];
58
+ for (const root of roots) {
59
+ const relativePaths = await listFiles(root.sourceRoot);
60
+ for (const relativePath of relativePaths) {
61
+ assets.push({
62
+ sourcePath: path.join(root.sourceRoot, relativePath),
63
+ targetPath: path.join(root.targetRoot, relativePath),
64
+ });
65
+ }
66
+ }
67
+
68
+ return assets.sort((left, right) => left.targetPath.localeCompare(right.targetPath));
69
+ }
70
+
71
+ async function listFiles(root: string, prefix = ''): Promise<string[]> {
72
+ const entries = await readdir(root, { withFileTypes: true });
73
+ const files: string[] = [];
74
+
75
+ for (const entry of entries) {
76
+ const nextRelative = prefix ? path.join(prefix, entry.name) : entry.name;
77
+ const fullPath = path.join(root, entry.name);
78
+ if (entry.isDirectory()) {
79
+ files.push(...(await listFiles(fullPath, nextRelative)));
80
+ continue;
81
+ }
82
+
83
+ if (entry.isFile()) {
84
+ files.push(nextRelative);
85
+ }
86
+ }
87
+
88
+ return files;
89
+ }
90
+
91
+ function createAsset(root: string, sourceRelativePath: string, targetPath: string): ScaffoldAsset {
92
+ return {
93
+ sourcePath: path.join(root, sourceRelativePath),
94
+ targetPath,
95
+ };
96
+ }