@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
@@ -0,0 +1,286 @@
1
+ import net from 'node:net';
2
+ import path from 'node:path';
3
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
4
+ import { createInterface, type Interface as ReadLineInterface } from 'node:readline';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+
7
+ import { resolveRuntimeCommand } from './runtime.ts';
8
+
9
+ interface RuntimeStream {
10
+ write(message: string): void;
11
+ }
12
+
13
+ export interface BackendRuntimeIo {
14
+ readonly stdout: RuntimeStream;
15
+ readonly stderr: RuntimeStream;
16
+ }
17
+
18
+ export interface BackendRuntimeSupervisorOptions {
19
+ readonly workspaceRoot: string;
20
+ readonly buildRoot: string;
21
+ readonly host: string;
22
+ readonly port?: number;
23
+ readonly env?: Record<string, string | undefined>;
24
+ readonly io: BackendRuntimeIo;
25
+ }
26
+
27
+ interface RuntimeProcessRecord {
28
+ readonly child: ChildProcessWithoutNullStreams;
29
+ readonly stdoutReader: ReadLineInterface;
30
+ readonly stderrReader: ReadLineInterface;
31
+ readonly exitPromise: Promise<number | null>;
32
+ expectedExit: boolean;
33
+ }
34
+
35
+ const DEFAULT_API_PORT = 4321;
36
+ const READY_TIMEOUT_MS = 15_000;
37
+ const READY_POLL_MS = 100;
38
+ const SOCKET_TIMEOUT_MS = 200;
39
+
40
+ export class BackendRuntimeSupervisor {
41
+ private readonly workspaceRoot: string;
42
+ private readonly entryFile: string;
43
+ private readonly displayHost: string;
44
+ private readonly env?: Record<string, string | undefined>;
45
+ private readonly io: BackendRuntimeIo;
46
+ private current?: RuntimeProcessRecord;
47
+ private queuedRestart: Promise<void> = Promise.resolve();
48
+ private isStopping = false;
49
+ private readonly requestedPort?: number;
50
+ private resolvedPort?: number;
51
+
52
+ public constructor(options: BackendRuntimeSupervisorOptions) {
53
+ this.workspaceRoot = path.resolve(options.workspaceRoot);
54
+ this.entryFile = path.join(path.resolve(options.buildRoot), 'index.js');
55
+ this.displayHost = normalizeDisplayHost(options.host);
56
+ this.env = options.env;
57
+ this.io = options.io;
58
+ this.requestedPort = options.port;
59
+ }
60
+
61
+ public async restart(): Promise<void> {
62
+ return await this.enqueue(async () => {
63
+ await this.restartInternal();
64
+ });
65
+ }
66
+
67
+ public async prepare(): Promise<void> {
68
+ await this.resolvePort();
69
+ }
70
+
71
+ public async stop(): Promise<void> {
72
+ this.isStopping = true;
73
+ await this.enqueue(async () => {
74
+ await this.stopCurrentProcess();
75
+ });
76
+ }
77
+
78
+ public getOrigin(): string {
79
+ const port = this.resolvedPort ?? this.requestedPort ?? DEFAULT_API_PORT;
80
+ return `http://${this.displayHost}:${port}`;
81
+ }
82
+
83
+ private async restartInternal(): Promise<void> {
84
+ if (this.isStopping) {
85
+ return;
86
+ }
87
+
88
+ const port = await this.resolvePort();
89
+ this.resolvedPort = port;
90
+ await this.stopCurrentProcess();
91
+ await this.startProcess(port);
92
+ }
93
+
94
+ private async enqueue(task: () => Promise<void>): Promise<void> {
95
+ const next = this.queuedRestart.catch(() => undefined).then(task);
96
+ this.queuedRestart = next.catch(() => undefined);
97
+ await next;
98
+ }
99
+
100
+ private async resolvePort(): Promise<number> {
101
+ if (this.resolvedPort !== undefined) {
102
+ return this.resolvedPort;
103
+ }
104
+
105
+ this.resolvedPort = await resolvePort(this.requestedPort);
106
+ return this.resolvedPort;
107
+ }
108
+
109
+ private async startProcess(port: number): Promise<void> {
110
+ const child = spawn(resolveRuntimeCommand(), [this.entryFile], {
111
+ cwd: this.workspaceRoot,
112
+ env: {
113
+ ...process.env,
114
+ ...this.env,
115
+ PORT: String(port),
116
+ API_BASE_URL: `http://${this.displayHost}:${port}`,
117
+ NODE_ENV: this.env?.NODE_ENV ?? process.env.NODE_ENV ?? 'development',
118
+ },
119
+ stdio: 'pipe',
120
+ });
121
+
122
+ const exitPromise = new Promise<number | null>((resolve, reject) => {
123
+ child.once('error', reject);
124
+ child.once('close', (code) => resolve(code));
125
+ });
126
+
127
+ const onStdoutLine = (line: string) => {
128
+ this.io.stdout.write(`[backend] ${line}\n`);
129
+ };
130
+
131
+ const onStderrLine = (line: string) => {
132
+ this.io.stderr.write(`[backend] ${line}\n`);
133
+ };
134
+
135
+ const stdoutReader = createInterface({ input: child.stdout, crlfDelay: Infinity });
136
+ stdoutReader.on('line', onStdoutLine);
137
+ const stderrReader = createInterface({ input: child.stderr, crlfDelay: Infinity });
138
+ stderrReader.on('line', onStderrLine);
139
+
140
+ const processRecord: RuntimeProcessRecord = {
141
+ child,
142
+ stdoutReader,
143
+ stderrReader,
144
+ exitPromise,
145
+ expectedExit: false,
146
+ };
147
+ this.current = processRecord;
148
+
149
+ let ready = false;
150
+
151
+ exitPromise.then((code) => {
152
+ if (!ready) {
153
+ return;
154
+ } else if (!processRecord.expectedExit && !this.isStopping && this.current === processRecord) {
155
+ this.io.stderr.write(`[webstir] backend runtime exited unexpectedly with code ${code ?? 'null'}.\n`);
156
+ }
157
+ }).finally(() => {
158
+ if (this.current === processRecord) {
159
+ this.current = undefined;
160
+ }
161
+ stdoutReader.close();
162
+ stderrReader.close();
163
+ });
164
+
165
+ try {
166
+ await waitForRuntimeReady(port, exitPromise);
167
+ ready = true;
168
+ } catch (error) {
169
+ processRecord.expectedExit = true;
170
+ child.kill('SIGTERM');
171
+ await exitPromise.catch(() => undefined);
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ private async stopCurrentProcess(): Promise<void> {
177
+ const current = this.current;
178
+ if (!current) {
179
+ return;
180
+ }
181
+
182
+ current.expectedExit = true;
183
+ current.child.kill('SIGTERM');
184
+ await current.exitPromise.catch(() => undefined);
185
+ if (this.current === current) {
186
+ this.current = undefined;
187
+ }
188
+ }
189
+ }
190
+
191
+ async function resolvePort(requestedPort?: number): Promise<number> {
192
+ if (requestedPort !== undefined) {
193
+ if (!(await isPortAvailable(requestedPort))) {
194
+ throw new Error(`Port ${requestedPort} is already in use.`);
195
+ }
196
+
197
+ return requestedPort;
198
+ }
199
+
200
+ return await findOpenPort(DEFAULT_API_PORT);
201
+ }
202
+
203
+ async function findOpenPort(startPort: number, attempts = 20): Promise<number> {
204
+ let candidate = startPort;
205
+ for (let index = 0; index < attempts; index += 1) {
206
+ // eslint-disable-next-line no-await-in-loop
207
+ if (await isPortAvailable(candidate)) {
208
+ return candidate;
209
+ }
210
+ candidate += 1;
211
+ }
212
+
213
+ throw new Error(`Unable to find an open port starting at ${startPort}.`);
214
+ }
215
+
216
+ function isPortAvailable(port: number): Promise<boolean> {
217
+ return new Promise((resolve) => {
218
+ const server = net.createServer();
219
+ server.once('error', () => {
220
+ server.close(() => resolve(false));
221
+ });
222
+ server.once('listening', () => {
223
+ server.close(() => resolve(true));
224
+ });
225
+ server.listen(port, '127.0.0.1');
226
+ });
227
+ }
228
+
229
+ function normalizeDisplayHost(host: string): string {
230
+ return host === '0.0.0.0' ? '127.0.0.1' : host;
231
+ }
232
+
233
+ async function waitForRuntimeReady(
234
+ port: number,
235
+ exitPromise: Promise<number | null>
236
+ ): Promise<void> {
237
+ const abortController = new AbortController();
238
+
239
+ try {
240
+ await Promise.race([
241
+ waitForPortOpen(port, abortController.signal),
242
+ exitPromise.then((code) => {
243
+ throw new Error(`Backend runtime exited before it became ready (code ${code ?? 'null'}).`);
244
+ }),
245
+ delay(READY_TIMEOUT_MS).then(() => {
246
+ throw new Error(`Backend runtime did not become ready within ${READY_TIMEOUT_MS}ms.`);
247
+ }),
248
+ ]);
249
+ } finally {
250
+ abortController.abort();
251
+ }
252
+ }
253
+
254
+ async function waitForPortOpen(port: number, signal: AbortSignal): Promise<void> {
255
+ while (!signal.aborted) {
256
+ if (await canConnectToPort(port)) {
257
+ return;
258
+ }
259
+
260
+ try {
261
+ await delay(READY_POLL_MS, undefined, { signal });
262
+ } catch (error) {
263
+ if (signal.aborted) {
264
+ return;
265
+ }
266
+
267
+ throw error;
268
+ }
269
+ }
270
+ }
271
+
272
+ function canConnectToPort(port: number): Promise<boolean> {
273
+ return new Promise((resolve) => {
274
+ const socket = net.createConnection({ host: '127.0.0.1', port });
275
+
276
+ const settle = (value: boolean) => {
277
+ socket.removeAllListeners();
278
+ socket.destroy();
279
+ resolve(value);
280
+ };
281
+
282
+ socket.once('connect', () => settle(true));
283
+ socket.once('error', () => settle(false));
284
+ socket.setTimeout(SOCKET_TIMEOUT_MS, () => settle(false));
285
+ });
286
+ }
@@ -0,0 +1,12 @@
1
+ import type { BuildTargetKind, WorkspaceMode } from './types.ts';
2
+
3
+ const BUILD_PLANS = {
4
+ spa: ['frontend'],
5
+ ssg: ['frontend'],
6
+ api: ['backend'],
7
+ full: ['frontend', 'backend'],
8
+ } as const satisfies Record<WorkspaceMode, readonly BuildTargetKind[]>;
9
+
10
+ export function createBuildPlan(mode: WorkspaceMode): readonly BuildTargetKind[] {
11
+ return BUILD_PLANS[mode];
12
+ }
package/src/build.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type {
2
+ CommandExecutionResult,
3
+ } from './types.ts';
4
+ import { runCommand, type RunCommandOptions } from './execute.ts';
5
+
6
+ export type RunBuildOptions = RunCommandOptions;
7
+
8
+ export async function runBuild(options: RunBuildOptions): Promise<CommandExecutionResult> {
9
+ return await runCommand('build', options);
10
+ }