@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/init.ts ADDED
@@ -0,0 +1,339 @@
1
+ import path from 'node:path';
2
+ import { cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { getModeScaffoldAssets, getRootScaffoldAssets } from './init-assets.ts';
7
+ import { monorepoRoot } from './paths.ts';
8
+ import type { WorkspaceMode } from './types.ts';
9
+
10
+ const PACKAGE_MANAGER = 'bun@1.3.5';
11
+ const REPO_WORKSPACE_PATTERNS = [
12
+ 'packages/contracts/*',
13
+ 'packages/tooling/*',
14
+ 'orchestrators/bun',
15
+ 'apps/*',
16
+ 'examples/demos/*',
17
+ 'examples/demos/ssg/*',
18
+ ] as const;
19
+
20
+ const MODE_DESCRIPTIONS: Record<WorkspaceMode, string> = {
21
+ ssg: 'Static site (SSG) workspace for Webstir.',
22
+ spa: 'SPA frontend workspace for Webstir.',
23
+ api: 'Backend API workspace for Webstir.',
24
+ full: 'Full-stack workspace for Webstir.',
25
+ };
26
+
27
+ export interface RunInitOptions {
28
+ readonly args: readonly string[];
29
+ readonly cwd?: string;
30
+ readonly workspaceRoot?: string;
31
+ }
32
+
33
+ export interface InitResult {
34
+ readonly workspaceRoot: string;
35
+ readonly mode: WorkspaceMode;
36
+ readonly packageName: string;
37
+ readonly changes: readonly string[];
38
+ }
39
+
40
+ export async function runInit(options: RunInitOptions): Promise<InitResult> {
41
+ const request = parseInitRequest(options.args, options.workspaceRoot, options.cwd ?? process.cwd());
42
+ return scaffoldWorkspace(request.mode, request.workspaceRoot, { force: false });
43
+ }
44
+
45
+ export async function scaffoldWorkspace(
46
+ mode: WorkspaceMode,
47
+ workspaceRoot: string,
48
+ options: { readonly force: boolean }
49
+ ): Promise<InitResult> {
50
+ if (existsSync(workspaceRoot) && !options.force && !(await isDirectoryEmpty(workspaceRoot))) {
51
+ throw new Error(`Refusing to initialize non-empty directory: ${workspaceRoot}`);
52
+ }
53
+
54
+ await mkdir(workspaceRoot, { recursive: true });
55
+
56
+ const packageName = resolvePackageName(workspaceRoot);
57
+ const dependencySpecs = await resolveDependencySpecs(workspaceRoot);
58
+ const changes: string[] = [];
59
+
60
+ for (const asset of getRootScaffoldAssets()) {
61
+ const targetPath = path.join(workspaceRoot, asset.targetPath);
62
+ await mkdir(path.dirname(targetPath), { recursive: true });
63
+ await cp(asset.sourcePath, targetPath, { force: true });
64
+ changes.push(toWorkspaceRelative(workspaceRoot, targetPath));
65
+ }
66
+
67
+ for (const asset of await getModeScaffoldAssets(mode)) {
68
+ const targetPath = path.join(workspaceRoot, asset.targetPath);
69
+ await mkdir(path.dirname(targetPath), { recursive: true });
70
+ await cp(asset.sourcePath, targetPath, { force: true });
71
+ changes.push(toWorkspaceRelative(workspaceRoot, targetPath));
72
+ }
73
+
74
+ const packageJsonPath = path.join(workspaceRoot, 'package.json');
75
+ await writeFile(
76
+ packageJsonPath,
77
+ `${JSON.stringify(createPackageJson(mode, workspaceRoot, packageName, dependencySpecs), null, 2)}\n`,
78
+ 'utf8'
79
+ );
80
+ changes.push('package.json');
81
+
82
+ const baseTsconfigPath = path.join(workspaceRoot, 'base.tsconfig.json');
83
+ await writeFile(baseTsconfigPath, `${JSON.stringify(createBaseTsconfig(mode), null, 2)}\n`, 'utf8');
84
+ changes.push('base.tsconfig.json');
85
+
86
+ return {
87
+ workspaceRoot,
88
+ mode,
89
+ packageName,
90
+ changes: uniqueSorted(changes),
91
+ };
92
+ }
93
+
94
+ function parseInitRequest(
95
+ args: readonly string[],
96
+ workspaceOverride: string | undefined,
97
+ cwd: string
98
+ ): { readonly mode: WorkspaceMode; readonly workspaceRoot: string } {
99
+ const [firstArg, secondArg] = args;
100
+
101
+ if (workspaceOverride) {
102
+ if (!firstArg) {
103
+ throw new Error('Usage: webstir init <mode> --workspace <path> or webstir init <mode> <directory>.');
104
+ }
105
+
106
+ return {
107
+ mode: parseWorkspaceMode(firstArg),
108
+ workspaceRoot: path.resolve(cwd, workspaceOverride),
109
+ };
110
+ }
111
+
112
+ if (!firstArg) {
113
+ throw new Error('Usage: webstir init <mode> <directory> or webstir init <directory>.');
114
+ }
115
+
116
+ if (!secondArg) {
117
+ return {
118
+ mode: 'full',
119
+ workspaceRoot: path.resolve(cwd, firstArg),
120
+ };
121
+ }
122
+
123
+ return {
124
+ mode: parseWorkspaceMode(firstArg),
125
+ workspaceRoot: path.resolve(cwd, secondArg),
126
+ };
127
+ }
128
+
129
+ function parseWorkspaceMode(value: string): WorkspaceMode {
130
+ const normalized = value.trim().toLowerCase();
131
+ if (normalized === 'ssg' || normalized === 'spa' || normalized === 'api' || normalized === 'full') {
132
+ return normalized;
133
+ }
134
+
135
+ if (normalized === 'fullstack') {
136
+ return 'full';
137
+ }
138
+
139
+ throw new Error(`Unknown init mode "${value}". Expected ssg, spa, api, or full.`);
140
+ }
141
+
142
+ async function isRepoWorkspacePath(workspaceRoot: string): Promise<boolean> {
143
+ if (!monorepoRoot) {
144
+ return false;
145
+ }
146
+
147
+ const relative = path.relative(monorepoRoot, workspaceRoot).replaceAll(path.sep, '/');
148
+ if (!relative || relative.startsWith('..')) {
149
+ return false;
150
+ }
151
+
152
+ return REPO_WORKSPACE_PATTERNS.some((pattern) => matchesWorkspacePattern(relative, pattern));
153
+ }
154
+
155
+ async function resolveDependencySpecs(workspaceRoot: string): Promise<Record<string, string>> {
156
+ if (await isRepoWorkspacePath(workspaceRoot)) {
157
+ return {
158
+ '@webstir-io/webstir-frontend': 'workspace:*',
159
+ '@webstir-io/webstir-backend': 'workspace:*',
160
+ '@webstir-io/webstir-testing': 'workspace:*',
161
+ };
162
+ }
163
+
164
+ return {
165
+ '@webstir-io/webstir-frontend': await readInstalledPackageVersion('@webstir-io/webstir-frontend'),
166
+ '@webstir-io/webstir-backend': await readInstalledPackageVersion('@webstir-io/webstir-backend'),
167
+ '@webstir-io/webstir-testing': await readInstalledPackageVersion('@webstir-io/webstir-testing'),
168
+ };
169
+ }
170
+
171
+ async function readPackageVersion(packageJsonPath: string): Promise<string> {
172
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as { readonly version?: string };
173
+ if (!packageJson.version) {
174
+ throw new Error(`Missing version in ${packageJsonPath}`);
175
+ }
176
+
177
+ return `^${packageJson.version}`;
178
+ }
179
+
180
+ function createPackageJson(
181
+ mode: WorkspaceMode,
182
+ workspaceRoot: string,
183
+ packageName: string,
184
+ dependencySpecs: Record<string, string>
185
+ ): Record<string, unknown> {
186
+ const dependencies: Record<string, string> = {
187
+ '@webstir-io/webstir-testing': dependencySpecs['@webstir-io/webstir-testing'],
188
+ };
189
+
190
+ if (mode === 'ssg' || mode === 'spa' || mode === 'full') {
191
+ dependencies['@webstir-io/webstir-frontend'] = dependencySpecs['@webstir-io/webstir-frontend'];
192
+ }
193
+
194
+ if (mode === 'api' || mode === 'full') {
195
+ dependencies['@webstir-io/webstir-backend'] = dependencySpecs['@webstir-io/webstir-backend'];
196
+ }
197
+
198
+ return {
199
+ name: packageName,
200
+ version: '1.0.0',
201
+ private: true,
202
+ type: 'module',
203
+ description: resolveDescription(mode, workspaceRoot),
204
+ dependencies,
205
+ devDependencies: {
206
+ '@types/node': '^20.0.0',
207
+ autoprefixer: '^10.4.20',
208
+ esbuild: '^0.25.0',
209
+ typescript: '^5.4.0',
210
+ },
211
+ packageManager: PACKAGE_MANAGER,
212
+ browserslist: [
213
+ 'last 2 chrome versions',
214
+ 'last 2 firefox versions',
215
+ 'last 2 safari major versions',
216
+ 'iOS >= 14',
217
+ 'not dead',
218
+ ],
219
+ webstir: {
220
+ mode,
221
+ moduleManifest: {},
222
+ },
223
+ };
224
+ }
225
+
226
+ function createBaseTsconfig(mode: WorkspaceMode): Record<string, unknown> {
227
+ const references = [];
228
+ if (mode !== 'api') {
229
+ if (mode !== 'ssg' && mode !== 'spa') {
230
+ references.push({ path: 'src/shared' });
231
+ references.push({ path: 'src/frontend' });
232
+ references.push({ path: 'src/backend' });
233
+ } else if (mode === 'spa') {
234
+ references.push({ path: 'src/shared' });
235
+ references.push({ path: 'src/frontend' });
236
+ } else {
237
+ references.push({ path: 'src/frontend' });
238
+ }
239
+ } else {
240
+ references.push({ path: 'src/shared' });
241
+ references.push({ path: 'src/backend' });
242
+ }
243
+
244
+ return {
245
+ files: [],
246
+ references,
247
+ compilerOptions: {
248
+ target: 'ES2022',
249
+ module: 'esnext',
250
+ moduleResolution: 'node',
251
+ strict: true,
252
+ esModuleInterop: true,
253
+ skipLibCheck: true,
254
+ forceConsistentCasingInFileNames: true,
255
+ sourceMap: true,
256
+ declaration: false,
257
+ removeComments: true,
258
+ typeRoots: [
259
+ './types',
260
+ './node_modules/@types',
261
+ ],
262
+ inlineSources: true,
263
+ },
264
+ };
265
+ }
266
+
267
+ function resolveDescription(mode: WorkspaceMode, workspaceRoot: string): string {
268
+ const relative = monorepoRoot
269
+ ? path.relative(monorepoRoot, workspaceRoot).replaceAll(path.sep, '/')
270
+ : '';
271
+ if (relative === 'examples/demos/full') {
272
+ return 'Webstir frontend defaults and tooling';
273
+ }
274
+
275
+ return MODE_DESCRIPTIONS[mode];
276
+ }
277
+
278
+ function resolvePackageName(workspaceRoot: string): string {
279
+ const relative = monorepoRoot
280
+ ? path.relative(monorepoRoot, workspaceRoot).replaceAll(path.sep, '/')
281
+ : '';
282
+ const known = getKnownWorkspacePackageName(relative);
283
+ if (known) {
284
+ return known;
285
+ }
286
+
287
+ return sanitizePackageName(path.basename(workspaceRoot));
288
+ }
289
+
290
+ async function readInstalledPackageVersion(packageName: string): Promise<string> {
291
+ const packageJsonUrl = import.meta.resolve(`${packageName}/package.json`);
292
+ const packageJsonPath = fileURLToPath(packageJsonUrl);
293
+ return await readPackageVersion(packageJsonPath);
294
+ }
295
+
296
+ function getKnownWorkspacePackageName(relativePath: string): string | undefined {
297
+ switch (relativePath) {
298
+ case 'examples/demos/spa':
299
+ return 'webstir-demo-spa';
300
+ case 'examples/demos/api':
301
+ return 'webstir-demo-api';
302
+ case 'examples/demos/full':
303
+ return 'webstir-demo-full';
304
+ case 'examples/demos/ssg/base':
305
+ return 'webstir-demo-ssg-base';
306
+ case 'examples/demos/ssg/site':
307
+ return 'webstir-demo-ssg-site';
308
+ default:
309
+ return undefined;
310
+ }
311
+ }
312
+
313
+ function sanitizePackageName(value: string): string {
314
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
315
+ return normalized || 'webstir-project';
316
+ }
317
+
318
+ async function isDirectoryEmpty(directoryPath: string): Promise<boolean> {
319
+ const entries = await readdir(directoryPath);
320
+ return entries.length === 0;
321
+ }
322
+
323
+ function matchesWorkspacePattern(relativePath: string, pattern: string): boolean {
324
+ const relativeSegments = relativePath.split('/').filter(Boolean);
325
+ const patternSegments = pattern.split('/').filter(Boolean);
326
+ if (relativeSegments.length !== patternSegments.length) {
327
+ return false;
328
+ }
329
+
330
+ return patternSegments.every((segment, index) => segment === '*' || segment === relativeSegments[index]);
331
+ }
332
+
333
+ function uniqueSorted(values: readonly string[]): string[] {
334
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
335
+ }
336
+
337
+ function toWorkspaceRelative(workspaceRoot: string, absolutePath: string): string {
338
+ return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, '/');
339
+ }
package/src/paths.ts ADDED
@@ -0,0 +1,26 @@
1
+ import path from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const here = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ export const packageRoot = path.resolve(here, '..');
8
+ export const assetsRoot = path.join(packageRoot, 'assets');
9
+
10
+ export const monorepoRoot = resolveMonorepoRoot();
11
+ export const repoRoot = monorepoRoot ?? path.resolve(packageRoot, '..', '..');
12
+
13
+ function resolveMonorepoRoot(): string | null {
14
+ const candidate = path.resolve(packageRoot, '..', '..');
15
+ const packageJsonPath = path.join(candidate, 'package.json');
16
+ if (!existsSync(packageJsonPath)) {
17
+ return null;
18
+ }
19
+
20
+ try {
21
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { name?: string };
22
+ return packageJson.name === 'webstir' ? candidate : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
@@ -0,0 +1,88 @@
1
+ import path from 'node:path';
2
+ import { access } from 'node:fs/promises';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ import type { BuildProvider, BuildTargetKind } from './types.ts';
6
+ import { monorepoRoot } from './paths.ts';
7
+ import { resolveRuntimeCommand } from './runtime.ts';
8
+
9
+ let localPackageBuildPromise: Promise<void> | null = null;
10
+
11
+ export async function loadProvider(kind: BuildTargetKind): Promise<BuildProvider> {
12
+ await ensureLocalPackageArtifacts();
13
+ if (kind === 'frontend') {
14
+ const mod = await import('@webstir-io/webstir-frontend') as { frontendProvider: BuildProvider };
15
+ return mod.frontendProvider;
16
+ }
17
+
18
+ const mod = await import('@webstir-io/webstir-backend') as { backendProvider: BuildProvider };
19
+ return mod.backendProvider;
20
+ }
21
+
22
+ export async function ensureLocalPackageArtifacts(): Promise<void> {
23
+ if (!monorepoRoot) {
24
+ return;
25
+ }
26
+
27
+ const requiredEntries = [
28
+ path.join(monorepoRoot, 'packages', 'contracts', 'module-contract', 'dist', 'index.js'),
29
+ path.join(monorepoRoot, 'packages', 'contracts', 'testing-contract', 'dist', 'index.js'),
30
+ path.join(monorepoRoot, 'packages', 'tooling', 'webstir-frontend', 'dist', 'index.js'),
31
+ path.join(monorepoRoot, 'packages', 'tooling', 'webstir-frontend', 'dist', 'cli.js'),
32
+ path.join(monorepoRoot, 'packages', 'tooling', 'webstir-backend', 'dist', 'index.js'),
33
+ path.join(monorepoRoot, 'packages', 'tooling', 'webstir-backend', 'dist', 'watch.js'),
34
+ path.join(monorepoRoot, 'packages', 'tooling', 'webstir-testing', 'dist', 'index.js'),
35
+ ];
36
+
37
+ try {
38
+ await Promise.all(requiredEntries.map(async (entry) => await access(entry)));
39
+ return;
40
+ } catch {
41
+ // Fall through to the build step.
42
+ }
43
+
44
+ if (!localPackageBuildPromise) {
45
+ localPackageBuildPromise = buildLocalPackages();
46
+ }
47
+
48
+ await localPackageBuildPromise;
49
+ }
50
+
51
+ async function buildLocalPackages(): Promise<void> {
52
+ const packages = [
53
+ '@webstir-io/module-contract',
54
+ '@webstir-io/testing-contract',
55
+ '@webstir-io/webstir-frontend',
56
+ '@webstir-io/webstir-backend',
57
+ '@webstir-io/webstir-testing',
58
+ ];
59
+
60
+ for (const packageName of packages) {
61
+ await runRuntimeCommand(['run', '--filter', packageName, 'build']);
62
+ }
63
+ }
64
+
65
+ async function runRuntimeCommand(args: readonly string[]): Promise<void> {
66
+ const cwd = monorepoRoot;
67
+ if (!cwd) {
68
+ return;
69
+ }
70
+
71
+ await new Promise<void>((resolve, reject) => {
72
+ const child = spawn(resolveRuntimeCommand(), args, {
73
+ cwd,
74
+ env: process.env,
75
+ stdio: 'inherit',
76
+ });
77
+
78
+ child.once('error', reject);
79
+ child.once('close', (code: number | null) => {
80
+ if (code === 0) {
81
+ resolve();
82
+ return;
83
+ }
84
+
85
+ reject(new Error(`Command failed with exit code ${code}: ${args.join(' ')}`));
86
+ });
87
+ });
88
+ }
package/src/publish.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { CommandExecutionResult } from './types.ts';
2
+ import { runCommand, type RunCommandOptions } from './execute.ts';
3
+
4
+ export type RunPublishOptions = RunCommandOptions;
5
+
6
+ export async function runPublish(options: RunPublishOptions): Promise<CommandExecutionResult> {
7
+ return await runCommand('publish', options);
8
+ }
package/src/refresh.ts ADDED
@@ -0,0 +1,56 @@
1
+ import path from 'node:path';
2
+ import { mkdir, readdir, rm } from 'node:fs/promises';
3
+
4
+ import { scaffoldWorkspace } from './init.ts';
5
+ import type { WorkspaceMode } from './types.ts';
6
+
7
+ export interface RunRefreshOptions {
8
+ readonly workspaceRoot: string;
9
+ readonly args: readonly string[];
10
+ readonly cwd?: string;
11
+ }
12
+
13
+ export interface RefreshResult {
14
+ readonly workspaceRoot: string;
15
+ readonly mode: WorkspaceMode;
16
+ readonly changes: readonly string[];
17
+ }
18
+
19
+ export async function runRefresh(options: RunRefreshOptions): Promise<RefreshResult> {
20
+ const workspaceRoot = path.resolve(options.cwd ?? process.cwd(), options.workspaceRoot);
21
+ const modeToken = options.args[0];
22
+ if (!modeToken) {
23
+ throw new Error('Usage: webstir refresh <mode> --workspace <path>.');
24
+ }
25
+
26
+ const mode = parseWorkspaceMode(modeToken);
27
+ await mkdir(workspaceRoot, { recursive: true });
28
+ await emptyDirectory(workspaceRoot);
29
+
30
+ const result = await scaffoldWorkspace(mode, workspaceRoot, { force: true });
31
+ return {
32
+ workspaceRoot: result.workspaceRoot,
33
+ mode: result.mode,
34
+ changes: result.changes,
35
+ };
36
+ }
37
+
38
+ async function emptyDirectory(directoryPath: string): Promise<void> {
39
+ const entries = await readdir(directoryPath, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ await rm(path.join(directoryPath, entry.name), { recursive: true, force: true });
42
+ }
43
+ }
44
+
45
+ function parseWorkspaceMode(value: string): WorkspaceMode {
46
+ const normalized = value.trim().toLowerCase();
47
+ if (normalized === 'ssg' || normalized === 'spa' || normalized === 'api' || normalized === 'full') {
48
+ return normalized;
49
+ }
50
+
51
+ if (normalized === 'fullstack') {
52
+ return 'full';
53
+ }
54
+
55
+ throw new Error(`Unknown refresh mode "${value}". Expected ssg, spa, api, or full.`);
56
+ }