@webstir-io/webstir 0.1.0 → 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
@@ -1,20 +1,91 @@
1
+ import type {
2
+ AddJobOptions,
3
+ AddRouteOptions,
4
+ UpdateRouteContractOptions as BackendUpdateRouteContractOptions,
5
+ } from '@webstir-io/webstir-backend';
6
+
1
7
  import type { AddCommandResult } from './add.ts';
2
8
 
3
- import { runAddJob, runAddRoute } from '@webstir-io/webstir-backend';
9
+ import { monorepoRoot } from './paths.ts';
4
10
 
5
11
  export interface RunAddBackendOptions {
6
12
  readonly workspaceRoot: string;
7
13
  readonly rawArgs: readonly string[];
8
14
  }
9
15
 
16
+ export type AddRouteScaffoldOptions = AddRouteOptions;
17
+ export type AddJobScaffoldOptions = AddJobOptions;
18
+ export type UpdateRouteContractOptions = BackendUpdateRouteContractOptions;
19
+
10
20
  export async function runAddRouteCommand(options: RunAddBackendOptions): Promise<AddCommandResult> {
11
- const parsed = parseBackendCommandArgs(options.rawArgs, {
21
+ return await runAddRouteScaffold({
22
+ workspaceRoot: options.workspaceRoot,
23
+ ...parseAddRouteScaffoldArgs(options.rawArgs),
24
+ });
25
+ }
26
+
27
+ export async function runAddRouteScaffold(
28
+ options: AddRouteScaffoldOptions,
29
+ ): Promise<AddCommandResult> {
30
+ const backendAdd = await loadBackendAddModule();
31
+ const result = await backendAdd.runAddRoute(options);
32
+
33
+ return {
34
+ workspaceRoot: options.workspaceRoot,
35
+ subject: 'route',
36
+ target: result.target,
37
+ changes: result.changes,
38
+ };
39
+ }
40
+
41
+ export async function runAddJobCommand(options: RunAddBackendOptions): Promise<AddCommandResult> {
42
+ return await runAddJobScaffold({
43
+ workspaceRoot: options.workspaceRoot,
44
+ ...parseAddJobScaffoldArgs(options.rawArgs),
45
+ });
46
+ }
47
+
48
+ export async function runAddJobScaffold(options: AddJobScaffoldOptions): Promise<AddCommandResult> {
49
+ const backendAdd = await loadBackendAddModule();
50
+ const result = await backendAdd.runAddJob(options);
51
+
52
+ return {
53
+ workspaceRoot: options.workspaceRoot,
54
+ subject: 'job',
55
+ target: result.target,
56
+ changes: result.changes,
57
+ };
58
+ }
59
+
60
+ export async function runUpdateRouteContract(
61
+ options: UpdateRouteContractOptions,
62
+ ): Promise<AddCommandResult> {
63
+ const backendAdd = await loadBackendAddModule();
64
+ const result = await backendAdd.runUpdateRouteContract(options);
65
+
66
+ return {
67
+ workspaceRoot: options.workspaceRoot,
68
+ subject: 'route',
69
+ target: result.target,
70
+ changes: result.changes,
71
+ };
72
+ }
73
+
74
+ export function parseAddRouteScaffoldArgs(
75
+ rawArgs: readonly string[],
76
+ ): Omit<AddRouteScaffoldOptions, 'workspaceRoot'> {
77
+ const parsed = parseBackendCommandArgs(rawArgs, {
12
78
  valueFlags: new Set([
13
79
  '--method',
14
80
  '--path',
15
81
  '--summary',
16
82
  '--description',
17
83
  '--tags',
84
+ '--interaction',
85
+ '--session',
86
+ '--fragment-target',
87
+ '--fragment-selector',
88
+ '--fragment-mode',
18
89
  '--params-schema',
19
90
  '--query-schema',
20
91
  '--body-schema',
@@ -23,30 +94,37 @@ export async function runAddRouteCommand(options: RunAddBackendOptions): Promise
23
94
  '--response-status',
24
95
  '--response-headers-schema',
25
96
  ]),
26
- booleanFlags: new Set(['--fastify']),
97
+ booleanFlags: new Set(['--session-write', '--form-urlencoded', '--csrf']),
27
98
  });
28
99
 
29
100
  const name = parsed.positionals[0];
30
101
  if (!name) {
31
102
  throw new Error(
32
- 'Usage: webstir add-route <name> --workspace <path> [--method <METHOD>] [--path <path>] [--fastify].'
103
+ 'Usage: webstir add-route <name> --workspace <path> [--method <METHOD>] [--path <path>].',
33
104
  );
34
105
  }
35
106
 
36
- const tags = parsed.values.get('--tags')
107
+ const tags = parsed.values
108
+ .get('--tags')
37
109
  ?.split(',')
38
110
  .map((tag) => tag.trim())
39
111
  .filter(Boolean);
40
112
 
41
- const result = await runAddRoute({
42
- workspaceRoot: options.workspaceRoot,
113
+ return {
43
114
  name,
44
115
  method: parsed.values.get('--method'),
45
116
  path: parsed.values.get('--path'),
46
- fastify: parsed.booleans.has('--fastify'),
47
117
  summary: parsed.values.get('--summary'),
48
118
  description: parsed.values.get('--description'),
49
119
  tags,
120
+ interaction: parsed.values.get('--interaction'),
121
+ sessionMode: parsed.values.get('--session'),
122
+ sessionWrite: parsed.booleans.has('--session-write'),
123
+ formUrlEncoded: parsed.booleans.has('--form-urlencoded'),
124
+ formCsrf: parsed.booleans.has('--csrf'),
125
+ fragmentTarget: parsed.values.get('--fragment-target'),
126
+ fragmentSelector: parsed.values.get('--fragment-selector'),
127
+ fragmentMode: parsed.values.get('--fragment-mode'),
50
128
  paramsSchema: parsed.values.get('--params-schema'),
51
129
  querySchema: parsed.values.get('--query-schema'),
52
130
  bodySchema: parsed.values.get('--body-schema'),
@@ -54,18 +132,13 @@ export async function runAddRouteCommand(options: RunAddBackendOptions): Promise
54
132
  responseSchema: parsed.values.get('--response-schema'),
55
133
  responseStatus: parsed.values.get('--response-status'),
56
134
  responseHeadersSchema: parsed.values.get('--response-headers-schema'),
57
- });
58
-
59
- return {
60
- workspaceRoot: options.workspaceRoot,
61
- subject: 'route',
62
- target: result.target,
63
- changes: result.changes,
64
135
  };
65
136
  }
66
137
 
67
- export async function runAddJobCommand(options: RunAddBackendOptions): Promise<AddCommandResult> {
68
- const parsed = parseBackendCommandArgs(options.rawArgs, {
138
+ export function parseAddJobScaffoldArgs(
139
+ rawArgs: readonly string[],
140
+ ): Omit<AddJobScaffoldOptions, 'workspaceRoot'> {
141
+ const parsed = parseBackendCommandArgs(rawArgs, {
69
142
  valueFlags: new Set(['--schedule', '--description', '--priority']),
70
143
  booleanFlags: new Set(),
71
144
  });
@@ -75,19 +148,11 @@ export async function runAddJobCommand(options: RunAddBackendOptions): Promise<A
75
148
  throw new Error('Usage: webstir add-job <name> --workspace <path> [--schedule <expression>].');
76
149
  }
77
150
 
78
- const result = await runAddJob({
79
- workspaceRoot: options.workspaceRoot,
151
+ return {
80
152
  name,
81
153
  schedule: parsed.values.get('--schedule'),
82
154
  description: parsed.values.get('--description'),
83
155
  priority: parsed.values.get('--priority'),
84
- });
85
-
86
- return {
87
- workspaceRoot: options.workspaceRoot,
88
- subject: 'job',
89
- target: result.target,
90
- changes: result.changes,
91
156
  };
92
157
  }
93
158
 
@@ -102,7 +167,70 @@ interface ParsedBackendCommandArgs {
102
167
  readonly booleans: ReadonlySet<string>;
103
168
  }
104
169
 
105
- function parseBackendCommandArgs(rawArgs: readonly string[], spec: ParseSpec): ParsedBackendCommandArgs {
170
+ interface BackendAddResult {
171
+ readonly target: string;
172
+ readonly changes: readonly string[];
173
+ }
174
+
175
+ interface BackendAddModule {
176
+ readonly runAddRoute: (options: AddRouteOptions) => Promise<BackendAddResult>;
177
+ readonly runAddJob: (options: AddJobOptions) => Promise<BackendAddResult>;
178
+ readonly runUpdateRouteContract: (
179
+ options: UpdateRouteContractOptions,
180
+ ) => Promise<BackendAddResult>;
181
+ }
182
+
183
+ let backendAddModulePromise: Promise<BackendAddModule> | null = null;
184
+
185
+ async function loadBackendAddModule(): Promise<BackendAddModule> {
186
+ if (backendAddModulePromise) {
187
+ return await backendAddModulePromise;
188
+ }
189
+
190
+ backendAddModulePromise = import('@webstir-io/webstir-backend').then(async (module) => {
191
+ if (
192
+ typeof module.runAddRoute === 'function' &&
193
+ typeof module.runAddJob === 'function' &&
194
+ typeof module.runUpdateRouteContract === 'function'
195
+ ) {
196
+ return module as BackendAddModule;
197
+ }
198
+
199
+ if (monorepoRoot) {
200
+ throw new Error(
201
+ 'Installed @webstir-io/webstir-backend package does not export runAddRoute/runAddJob/runUpdateRouteContract.',
202
+ );
203
+ }
204
+
205
+ const compat = await import('./add-backend-compat.ts');
206
+ return {
207
+ async runAddRoute(options: AddRouteOptions): Promise<BackendAddResult> {
208
+ return await compat.runAddRoute(options);
209
+ },
210
+ async runAddJob(options: AddJobOptions): Promise<BackendAddResult> {
211
+ return await compat.runAddJob({
212
+ workspaceRoot: options.workspaceRoot,
213
+ name: options.name,
214
+ schedule: options.schedule,
215
+ description: options.description,
216
+ ...(options.priority !== undefined ? { priority: String(options.priority) } : {}),
217
+ });
218
+ },
219
+ async runUpdateRouteContract(): Promise<BackendAddResult> {
220
+ throw new Error(
221
+ 'Installed @webstir-io/webstir-backend package does not export runUpdateRouteContract.',
222
+ );
223
+ },
224
+ };
225
+ });
226
+
227
+ return await backendAddModulePromise;
228
+ }
229
+
230
+ function parseBackendCommandArgs(
231
+ rawArgs: readonly string[],
232
+ spec: ParseSpec,
233
+ ): ParsedBackendCommandArgs {
106
234
  const positionals: string[] = [];
107
235
  const values = new Map<string, string>();
108
236
  const booleans = new Set<string>();
package/src/add.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
2
3
  import { readFile } from 'node:fs/promises';
3
4
  import { existsSync } from 'node:fs';
4
5
 
5
6
  import { runAddPage } from '@webstir-io/webstir-frontend';
6
- import { runAddTest } from '@webstir-io/webstir-testing';
7
+ import { monorepoRoot, packageRoot } from './paths.ts';
7
8
 
8
9
  export interface RunAddPageOptions {
9
10
  readonly workspaceRoot: string;
@@ -23,6 +24,12 @@ export interface AddCommandResult {
23
24
  readonly note?: string;
24
25
  }
25
26
 
27
+ interface AddTestInvocationResult {
28
+ readonly normalizedName: string;
29
+ readonly created: boolean;
30
+ readonly relativePath: string;
31
+ }
32
+
26
33
  export async function runAddPageCommand(options: RunAddPageOptions): Promise<AddCommandResult> {
27
34
  const pageName = options.args[0];
28
35
  if (!pageName) {
@@ -60,6 +67,7 @@ export async function runAddTestCommand(options: RunAddTestOptions): Promise<Add
60
67
  throw new Error('Usage: webstir add-test <name-or-path> --workspace <path>.');
61
68
  }
62
69
 
70
+ const runAddTest = await loadAddTestRunner();
63
71
  const result = await runAddTest({
64
72
  workspaceRoot: options.workspaceRoot,
65
73
  name: nameArg,
@@ -70,14 +78,111 @@ export async function runAddTestCommand(options: RunAddTestOptions): Promise<Add
70
78
  subject: 'test',
71
79
  target: result.normalizedName,
72
80
  changes: result.created ? [result.relativePath.replaceAll(path.sep, '/')] : [],
73
- note: result.created ? undefined : `File already exists: ${result.relativePath.replaceAll(path.sep, '/')}`,
81
+ note: result.created
82
+ ? undefined
83
+ : `File already exists: ${result.relativePath.replaceAll(path.sep, '/')}`,
84
+ };
85
+ }
86
+
87
+ async function loadAddTestRunner(): Promise<
88
+ (options: {
89
+ readonly workspaceRoot: string;
90
+ readonly name: string;
91
+ }) => Promise<AddTestInvocationResult>
92
+ > {
93
+ const mod = (await import('@webstir-io/webstir-testing')) as {
94
+ runAddTest?: (options: {
95
+ readonly workspaceRoot: string;
96
+ readonly name: string;
97
+ }) => Promise<AddTestInvocationResult>;
98
+ };
99
+ if (typeof mod.runAddTest === 'function') {
100
+ return mod.runAddTest;
101
+ }
102
+
103
+ if (monorepoRoot) {
104
+ throw new Error('Installed @webstir-io/webstir-testing package does not export runAddTest.');
105
+ }
106
+
107
+ // The published regular-install path exposes the add-test binary before it exposes the helper.
108
+ return async (options) => await runPublishedAddTestCli(options.workspaceRoot, options.name);
109
+ }
110
+
111
+ async function runPublishedAddTestCli(
112
+ workspaceRoot: string,
113
+ name: string,
114
+ ): Promise<AddTestInvocationResult> {
115
+ const target = resolveAddTestTarget(workspaceRoot, name);
116
+ const existedBefore = existsSync(target.absolutePath);
117
+ const binaryPath = path.join(
118
+ packageRoot,
119
+ '..',
120
+ '..',
121
+ '.bin',
122
+ process.platform === 'win32' ? 'webstir-testing-add.cmd' : 'webstir-testing-add',
123
+ );
124
+ const result = spawnSync(process.execPath, [binaryPath, name, '--workspace', workspaceRoot], {
125
+ cwd: workspaceRoot,
126
+ env: process.env,
127
+ encoding: 'utf8',
128
+ });
129
+
130
+ if (result.error) {
131
+ throw result.error;
132
+ }
133
+
134
+ if (result.status !== 0) {
135
+ const detail = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
136
+ throw new Error(
137
+ `Command failed (${result.status}): ${process.execPath} ${binaryPath} ${name} --workspace ${workspaceRoot}${detail ? `\n${detail}` : ''}`,
138
+ );
139
+ }
140
+
141
+ if (!existsSync(target.absolutePath)) {
142
+ throw new Error(`Expected add-test output at ${target.relativePath}`);
143
+ }
144
+
145
+ return {
146
+ normalizedName: target.normalizedName,
147
+ created: !existedBefore,
148
+ relativePath: target.relativePath,
149
+ };
150
+ }
151
+
152
+ function resolveAddTestTarget(
153
+ workspaceRoot: string,
154
+ rawName: string,
155
+ ): {
156
+ readonly normalizedName: string;
157
+ readonly relativePath: string;
158
+ readonly absolutePath: string;
159
+ } {
160
+ const normalizedName = rawName
161
+ .trim()
162
+ .replace(/\\/g, '/')
163
+ .replace(/(\.test\.ts)$/i, '');
164
+ const hasSlash = normalizedName.includes('/');
165
+
166
+ const relativePath = hasSlash
167
+ ? path.join(
168
+ 'src',
169
+ path.posix.dirname(normalizedName),
170
+ 'tests',
171
+ `${path.posix.basename(normalizedName)}.test.ts`,
172
+ )
173
+ : path.join('src', 'tests', `${normalizedName}.test.ts`);
174
+
175
+ return {
176
+ normalizedName,
177
+ relativePath,
178
+ absolutePath: path.join(workspaceRoot, relativePath),
74
179
  };
75
180
  }
76
181
 
77
182
  async function collectChangedFiles(
78
183
  workspaceRoot: string,
79
184
  absolutePaths: readonly string[],
80
- before: ReadonlyMap<string, string | null>
185
+ before: ReadonlyMap<string, string | null>,
81
186
  ): Promise<string[]> {
82
187
  const changes: string[] = [];
83
188
  for (const absolutePath of absolutePaths) {
@@ -90,7 +195,9 @@ async function collectChangedFiles(
90
195
  return changes;
91
196
  }
92
197
 
93
- async function captureFileState(absolutePaths: readonly string[]): Promise<Map<string, string | null>> {
198
+ async function captureFileState(
199
+ absolutePaths: readonly string[],
200
+ ): Promise<Map<string, string | null>> {
94
201
  const state = new Map<string, string | null>();
95
202
  for (const absolutePath of absolutePaths) {
96
203
  state.set(absolutePath, await readFileIfExists(absolutePath));