@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,251 @@
1
+ import path from 'node:path';
2
+ import { watch, type FSWatcher } from 'node:fs';
3
+ import { readdir, stat } from 'node:fs/promises';
4
+
5
+ export type WorkspaceWatchEvent =
6
+ | { readonly type: 'change'; readonly path: string }
7
+ | { readonly type: 'reload'; readonly path?: string };
8
+
9
+ export interface WorkspaceWatcherOptions {
10
+ readonly workspaceRoot: string;
11
+ readonly debounceMs?: number;
12
+ readonly onEvent: (event: WorkspaceWatchEvent) => void;
13
+ }
14
+
15
+ const ROOT_TRIGGER_FILES = new Set(['package.json', 'base.tsconfig.json', 'types.global.d.ts']);
16
+ const IGNORED_DIRECTORIES = new Set(['.git', '.webstir', 'build', 'dist', 'node_modules']);
17
+
18
+ export class WorkspaceWatcher {
19
+ private readonly workspaceRoot: string;
20
+ private readonly treeRoots: readonly string[];
21
+ private readonly onEvent: (event: WorkspaceWatchEvent) => void;
22
+ private readonly debounceMs: number;
23
+ private readonly treeWatchers = new Map<string, FSWatcher>();
24
+ private readonly pendingChanges = new Set<string>();
25
+ private readonly pendingSyncs = new Map<string, NodeJS.Timeout>();
26
+ private rootWatcher?: FSWatcher;
27
+ private flushTimer?: NodeJS.Timeout;
28
+ private reloadPending = false;
29
+ private reloadPath?: string;
30
+
31
+ public constructor(options: WorkspaceWatcherOptions) {
32
+ this.workspaceRoot = path.resolve(options.workspaceRoot);
33
+ this.treeRoots = [
34
+ path.join(this.workspaceRoot, 'src'),
35
+ path.join(this.workspaceRoot, 'types'),
36
+ ];
37
+ this.onEvent = options.onEvent;
38
+ this.debounceMs = options.debounceMs ?? 75;
39
+ }
40
+
41
+ public async start(): Promise<void> {
42
+ this.watchRootDirectory();
43
+ for (const root of this.treeRoots) {
44
+ await this.syncTree(root);
45
+ }
46
+ }
47
+
48
+ public async stop(): Promise<void> {
49
+ if (this.flushTimer) {
50
+ clearTimeout(this.flushTimer);
51
+ this.flushTimer = undefined;
52
+ }
53
+
54
+ for (const timer of this.pendingSyncs.values()) {
55
+ clearTimeout(timer);
56
+ }
57
+ this.pendingSyncs.clear();
58
+
59
+ if (this.rootWatcher) {
60
+ this.rootWatcher.close();
61
+ this.rootWatcher = undefined;
62
+ }
63
+
64
+ for (const watcher of this.treeWatchers.values()) {
65
+ watcher.close();
66
+ }
67
+ this.treeWatchers.clear();
68
+ }
69
+
70
+ private watchRootDirectory(): void {
71
+ this.rootWatcher = watch(this.workspaceRoot, (_eventType, filename) => {
72
+ if (!filename) {
73
+ return;
74
+ }
75
+
76
+ const entryName = filename.toString();
77
+ if (ROOT_TRIGGER_FILES.has(entryName)) {
78
+ this.queueReload(path.join(this.workspaceRoot, entryName));
79
+ return;
80
+ }
81
+
82
+ if (entryName === 'src' || entryName === 'types') {
83
+ this.scheduleTreeSync(path.join(this.workspaceRoot, entryName));
84
+ }
85
+ });
86
+ }
87
+
88
+ private scheduleTreeSync(root: string): void {
89
+ const existing = this.pendingSyncs.get(root);
90
+ if (existing) {
91
+ clearTimeout(existing);
92
+ }
93
+
94
+ const timer = setTimeout(() => {
95
+ this.pendingSyncs.delete(root);
96
+ void this.syncTree(root);
97
+ }, this.debounceMs);
98
+
99
+ this.pendingSyncs.set(root, timer);
100
+ }
101
+
102
+ private async syncTree(root: string): Promise<void> {
103
+ const directories = await collectDirectories(root);
104
+ const current = new Set(directories);
105
+
106
+ for (const directory of directories) {
107
+ if (this.treeWatchers.has(directory)) {
108
+ continue;
109
+ }
110
+
111
+ this.watchDirectory(directory, root);
112
+ }
113
+
114
+ for (const watchedDirectory of Array.from(this.treeWatchers.keys())) {
115
+ if (watchedDirectory !== root && watchedDirectory.startsWith(`${root}${path.sep}`) && !current.has(watchedDirectory)) {
116
+ this.treeWatchers.get(watchedDirectory)?.close();
117
+ this.treeWatchers.delete(watchedDirectory);
118
+ }
119
+ }
120
+
121
+ if (!current.has(root) && this.treeWatchers.has(root)) {
122
+ this.treeWatchers.get(root)?.close();
123
+ this.treeWatchers.delete(root);
124
+ }
125
+ }
126
+
127
+ private watchDirectory(directory: string, root: string): void {
128
+ try {
129
+ const watcher = watch(directory, (eventType, filename) => {
130
+ const absolutePath = filename ? path.join(directory, filename.toString()) : undefined;
131
+
132
+ if (eventType === 'rename') {
133
+ this.queueReload(absolutePath);
134
+ this.scheduleTreeSync(root);
135
+ return;
136
+ }
137
+
138
+ if (!absolutePath) {
139
+ this.queueReload(root);
140
+ this.scheduleTreeSync(root);
141
+ return;
142
+ }
143
+
144
+ void this.handleFileChange(root, absolutePath);
145
+ });
146
+
147
+ watcher.once('error', () => {
148
+ watcher.close();
149
+ this.treeWatchers.delete(directory);
150
+ this.scheduleTreeSync(root);
151
+ });
152
+
153
+ this.treeWatchers.set(directory, watcher);
154
+ } catch {
155
+ // Ignore transient directories that vanish before the watcher attaches.
156
+ }
157
+ }
158
+
159
+ private async handleFileChange(root: string, absolutePath: string): Promise<void> {
160
+ const baseName = path.basename(absolutePath);
161
+ if (IGNORED_DIRECTORIES.has(baseName)) {
162
+ return;
163
+ }
164
+
165
+ this.scheduleTreeSync(root);
166
+
167
+ try {
168
+ const details = await stat(absolutePath);
169
+ if (details.isDirectory()) {
170
+ this.queueReload(absolutePath);
171
+ return;
172
+ }
173
+
174
+ this.queueChange(absolutePath);
175
+ } catch {
176
+ this.queueReload(absolutePath);
177
+ }
178
+ }
179
+
180
+ private queueChange(filePath: string): void {
181
+ if (this.reloadPending) {
182
+ return;
183
+ }
184
+
185
+ this.pendingChanges.add(path.resolve(filePath));
186
+ this.scheduleFlush();
187
+ }
188
+
189
+ private queueReload(filePath?: string): void {
190
+ this.reloadPending = true;
191
+ this.reloadPath = filePath ? path.resolve(filePath) : undefined;
192
+ this.pendingChanges.clear();
193
+ this.scheduleFlush();
194
+ }
195
+
196
+ private scheduleFlush(): void {
197
+ if (this.flushTimer) {
198
+ clearTimeout(this.flushTimer);
199
+ }
200
+
201
+ this.flushTimer = setTimeout(() => {
202
+ this.flushTimer = undefined;
203
+ this.flush();
204
+ }, this.debounceMs);
205
+ }
206
+
207
+ private flush(): void {
208
+ if (this.reloadPending) {
209
+ const reloadPath = this.reloadPath;
210
+ this.reloadPending = false;
211
+ this.reloadPath = undefined;
212
+ this.onEvent(reloadPath ? { type: 'reload', path: reloadPath } : { type: 'reload' });
213
+ return;
214
+ }
215
+
216
+ for (const filePath of Array.from(this.pendingChanges).sort()) {
217
+ this.onEvent({ type: 'change', path: filePath });
218
+ }
219
+ this.pendingChanges.clear();
220
+ }
221
+ }
222
+
223
+ async function collectDirectories(root: string): Promise<readonly string[]> {
224
+ try {
225
+ const details = await stat(root);
226
+ if (!details.isDirectory()) {
227
+ return [];
228
+ }
229
+ } catch {
230
+ return [];
231
+ }
232
+
233
+ const directories: string[] = [];
234
+ const stack = [path.resolve(root)];
235
+
236
+ while (stack.length > 0) {
237
+ const current = stack.pop()!;
238
+ directories.push(current);
239
+
240
+ const entries = await readdir(current, { withFileTypes: true });
241
+ for (const entry of entries) {
242
+ if (!entry.isDirectory() || IGNORED_DIRECTORIES.has(entry.name)) {
243
+ continue;
244
+ }
245
+
246
+ stack.push(path.join(current, entry.name));
247
+ }
248
+ }
249
+
250
+ return directories;
251
+ }
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ import {
5
+ SUPPORTED_WORKSPACE_MODES,
6
+ type WorkspaceDescriptor,
7
+ type WorkspaceMode,
8
+ } from './types.ts';
9
+
10
+ interface WorkspacePackageJson {
11
+ readonly name?: string;
12
+ readonly webstir?: {
13
+ readonly mode?: string;
14
+ };
15
+ }
16
+
17
+ export function parseWorkspaceMode(value: unknown): WorkspaceMode {
18
+ if (typeof value !== 'string') {
19
+ throw new Error('Workspace package.json is missing webstir.mode.');
20
+ }
21
+
22
+ const normalized = value.trim().toLowerCase();
23
+ if (SUPPORTED_WORKSPACE_MODES.includes(normalized as WorkspaceMode)) {
24
+ return normalized as WorkspaceMode;
25
+ }
26
+
27
+ throw new Error(
28
+ `Unsupported webstir.mode "${value}". Expected one of: ${SUPPORTED_WORKSPACE_MODES.join(', ')}.`
29
+ );
30
+ }
31
+
32
+ export async function readWorkspaceDescriptor(workspacePath: string): Promise<WorkspaceDescriptor> {
33
+ const root = path.resolve(workspacePath);
34
+ const packageJsonPath = path.join(root, 'package.json');
35
+
36
+ let rawPackageJson: string;
37
+ try {
38
+ rawPackageJson = await readFile(packageJsonPath, 'utf8');
39
+ } catch (error) {
40
+ throw new Error(`Workspace package.json not found at ${packageJsonPath}.`, { cause: error });
41
+ }
42
+
43
+ let packageJson: WorkspacePackageJson;
44
+ try {
45
+ packageJson = JSON.parse(rawPackageJson) as WorkspacePackageJson;
46
+ } catch (error) {
47
+ throw new Error(`Workspace package.json at ${packageJsonPath} is not valid JSON.`, { cause: error });
48
+ }
49
+
50
+ return {
51
+ root,
52
+ name: typeof packageJson.name === 'string' ? packageJson.name : path.basename(root),
53
+ mode: parseWorkspaceMode(packageJson.webstir?.mode),
54
+ };
55
+ }