@webstir-io/webstir 0.1.1 → 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.
- package/README.md +13 -0
- package/assets/deployment/docker/.dockerignore +7 -0
- package/assets/deployment/docker/Dockerfile +17 -0
- package/assets/deployment/docker/README.md +44 -0
- package/assets/deployment/docker/example.env +3 -0
- package/assets/features/client_nav/client_nav.ts +369 -264
- package/assets/features/client_nav/document_navigation.ts +344 -0
- package/assets/features/client_nav/form_enhancement.ts +275 -0
- package/assets/templates/api/src/backend/index.ts +71 -10
- package/assets/templates/api/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/backend/index.ts +71 -10
- package/assets/templates/full/src/backend/module.ts +515 -0
- package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
- package/assets/templates/full/src/backend/tsconfig.json +6 -1
- package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
- package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
- package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
- package/package.json +31 -13
- package/scripts/check-feature-projections.mjs +87 -0
- package/scripts/check-full-demo-sync.mjs +89 -0
- package/scripts/check-package-install.mjs +537 -0
- package/scripts/check-standalone-install.mjs +221 -0
- package/scripts/pack-standalone.mjs +52 -28
- package/scripts/publish.sh +9 -0
- package/scripts/run-tests.mjs +99 -0
- package/scripts/sync-assets.mjs +175 -17
- package/src/add-backend-compat.ts +628 -0
- package/src/add-backend.ts +155 -27
- package/src/add.ts +111 -4
- package/src/agent.ts +393 -0
- package/src/api-watch.ts +7 -4
- package/src/backend-inspect.ts +70 -2
- package/src/backend-runtime.ts +22 -14
- package/src/build.ts +1 -3
- package/src/bun-generated-frontend-watch.ts +209 -0
- package/src/bun-globals.d.ts +23 -0
- package/src/bun-spa-document.ts +310 -0
- package/src/bun-spa-routes.ts +159 -0
- package/src/bun-spa-watch.ts +29 -0
- package/src/bun-ssg-watch.ts +304 -0
- package/src/cli.ts +381 -50
- package/src/compile-tests.ts +37 -29
- package/src/dev-server.ts +214 -143
- package/src/doctor.ts +164 -0
- package/src/enable-assets.ts +18 -1
- package/src/enable.ts +133 -41
- package/src/execute.ts +28 -4
- package/src/external-workspace.ts +178 -0
- package/src/format.ts +296 -17
- package/src/frontend-inspect.ts +32 -0
- package/src/frontend-watch.ts +27 -102
- package/src/full-watch.ts +13 -18
- package/src/index.ts +7 -0
- package/src/init-assets.ts +41 -11
- package/src/init.ts +85 -71
- package/src/inspect.ts +112 -0
- package/src/mcp/run-cli-json.ts +46 -0
- package/src/mcp/server.ts +307 -0
- package/src/operations.ts +176 -0
- package/src/providers.ts +20 -18
- package/src/refresh.ts +29 -3
- package/src/repair.ts +110 -43
- package/src/runtime-filter.ts +41 -0
- package/src/runtime.ts +1 -1
- package/src/smoke.ts +48 -16
- package/src/test.ts +54 -16
- package/src/testing-runtime.ts +273 -0
- package/src/types.ts +1 -4
- package/src/watch-events.ts +46 -17
- package/src/watch.ts +5 -1
- package/src/workspace-watcher.ts +10 -6
- package/src/workspace.ts +4 -2
- package/src/watch-daemon-client.ts +0 -171
package/src/frontend-watch.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { startBunSpaFrontendWatch } from './bun-spa-watch.ts';
|
|
2
|
+
import { startBunSsgFrontendWatch } from './bun-ssg-watch.ts';
|
|
3
|
+
import type { DevServerAddress } from './dev-server.ts';
|
|
4
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
5
|
import type { WorkspaceDescriptor } from './types.ts';
|
|
8
6
|
import type { WatchIo, WatchOptions } from './watch.ts';
|
|
9
|
-
import { WorkspaceWatcher, type WorkspaceWatchEvent } from './workspace-watcher.ts';
|
|
10
7
|
|
|
11
8
|
export interface FrontendWatchSession {
|
|
12
9
|
readonly address: DevServerAddress;
|
|
@@ -14,31 +11,29 @@ export interface FrontendWatchSession {
|
|
|
14
11
|
stop(): Promise<void>;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
readonly server?: DevServer;
|
|
19
|
-
}
|
|
14
|
+
type FrontendWatchSessionOptions = WatchOptions;
|
|
20
15
|
|
|
21
16
|
export async function runFrontendWatch(
|
|
22
17
|
workspace: WorkspaceDescriptor,
|
|
23
18
|
options: WatchOptions,
|
|
24
|
-
io: WatchIo
|
|
19
|
+
io: WatchIo,
|
|
25
20
|
): Promise<void> {
|
|
26
21
|
const session = await startFrontendWatchSession(workspace, options, io);
|
|
27
22
|
|
|
28
23
|
io.stdout.write(
|
|
29
|
-
`[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${session.address.origin}\n
|
|
24
|
+
`[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${session.address.origin}\n`,
|
|
30
25
|
);
|
|
31
26
|
|
|
32
27
|
const stopSignal = createStopSignal();
|
|
33
28
|
|
|
34
29
|
try {
|
|
35
|
-
const
|
|
30
|
+
const sessionExitCode = await Promise.race([
|
|
36
31
|
session.waitForExit(),
|
|
37
32
|
stopSignal.promise.then(() => null),
|
|
38
33
|
]);
|
|
39
34
|
|
|
40
|
-
if (typeof
|
|
41
|
-
throw new Error(`Frontend watch
|
|
35
|
+
if (typeof sessionExitCode === 'number' && sessionExitCode !== 0) {
|
|
36
|
+
throw new Error(`Frontend watch session exited with code ${sessionExitCode}.`);
|
|
42
37
|
}
|
|
43
38
|
} finally {
|
|
44
39
|
stopSignal.dispose();
|
|
@@ -49,97 +44,27 @@ export async function runFrontendWatch(
|
|
|
49
44
|
export async function startFrontendWatchSession(
|
|
50
45
|
workspace: WorkspaceDescriptor,
|
|
51
46
|
options: FrontendWatchSessionOptions,
|
|
52
|
-
io: WatchIo
|
|
47
|
+
io: WatchIo,
|
|
53
48
|
): Promise<FrontendWatchSession> {
|
|
54
|
-
|
|
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
|
-
};
|
|
49
|
+
return await createFrontendWatchSession(workspace, options, io);
|
|
114
50
|
}
|
|
115
51
|
|
|
116
|
-
async function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;
|
|
52
|
+
async function createFrontendWatchSession(
|
|
53
|
+
workspace: WorkspaceDescriptor,
|
|
54
|
+
options: FrontendWatchSessionOptions,
|
|
55
|
+
_io: WatchIo,
|
|
56
|
+
): Promise<FrontendWatchSession> {
|
|
57
|
+
if (workspace.mode === 'ssg') {
|
|
58
|
+
return await startBunSsgFrontendWatch({
|
|
59
|
+
workspaceRoot: workspace.root,
|
|
60
|
+
host: options.host,
|
|
61
|
+
port: options.port,
|
|
62
|
+
});
|
|
142
63
|
}
|
|
143
64
|
|
|
144
|
-
await
|
|
65
|
+
return await startBunSpaFrontendWatch({
|
|
66
|
+
workspaceRoot: workspace.root,
|
|
67
|
+
host: options.host,
|
|
68
|
+
port: options.port,
|
|
69
|
+
});
|
|
145
70
|
}
|
package/src/full-watch.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { createServer } from 'node:net';
|
|
3
2
|
|
|
4
3
|
import { startApiWatchSession } from './api-watch.ts';
|
|
5
|
-
import {
|
|
6
|
-
import { startFrontendWatchSession } from './frontend-watch.ts';
|
|
4
|
+
import { startBunGeneratedFrontendWatch } from './bun-generated-frontend-watch.ts';
|
|
7
5
|
import { createStopSignal } from './stop-signal.ts';
|
|
8
6
|
import type { WorkspaceDescriptor } from './types.ts';
|
|
9
7
|
import type { WatchIo, WatchOptions } from './watch.ts';
|
|
@@ -11,34 +9,32 @@ import type { WatchIo, WatchOptions } from './watch.ts';
|
|
|
11
9
|
export async function runFullWatch(
|
|
12
10
|
workspace: WorkspaceDescriptor,
|
|
13
11
|
options: WatchOptions,
|
|
14
|
-
io: WatchIo
|
|
12
|
+
io: WatchIo,
|
|
15
13
|
): Promise<void> {
|
|
16
14
|
const backendPort = await allocateBackendPort();
|
|
17
15
|
const apiSession = await startApiWatchSession(workspace, { ...options, port: backendPort }, io);
|
|
18
|
-
|
|
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;
|
|
16
|
+
let frontendSession: Awaited<ReturnType<typeof startBunGeneratedFrontendWatch>> | undefined;
|
|
26
17
|
|
|
27
18
|
try {
|
|
28
|
-
frontendSession = await
|
|
19
|
+
frontendSession = await startBunGeneratedFrontendWatch({
|
|
20
|
+
workspaceRoot: workspace.root,
|
|
21
|
+
host: options.host,
|
|
22
|
+
port: options.port,
|
|
23
|
+
apiProxyOrigin: apiSession.origin,
|
|
24
|
+
});
|
|
29
25
|
io.stdout.write(
|
|
30
|
-
`[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${frontendSession.address.origin}\napi: ${apiSession.origin}\n
|
|
26
|
+
`[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${frontendSession.address.origin}\napi: ${apiSession.origin}\n`,
|
|
31
27
|
);
|
|
32
28
|
|
|
33
29
|
const stopSignal = createStopSignal();
|
|
34
30
|
try {
|
|
35
|
-
const
|
|
31
|
+
const sessionExitCode = await Promise.race([
|
|
36
32
|
frontendSession.waitForExit(),
|
|
37
33
|
stopSignal.promise.then(() => null),
|
|
38
34
|
]);
|
|
39
35
|
|
|
40
|
-
if (typeof
|
|
41
|
-
throw new Error(`Frontend watch
|
|
36
|
+
if (typeof sessionExitCode === 'number' && sessionExitCode !== 0) {
|
|
37
|
+
throw new Error(`Frontend watch session exited with code ${sessionExitCode}.`);
|
|
42
38
|
}
|
|
43
39
|
} finally {
|
|
44
40
|
stopSignal.dispose();
|
|
@@ -47,7 +43,6 @@ export async function runFullWatch(
|
|
|
47
43
|
if (frontendSession) {
|
|
48
44
|
await frontendSession.stop();
|
|
49
45
|
}
|
|
50
|
-
await server.stop();
|
|
51
46
|
await apiSession.stop();
|
|
52
47
|
}
|
|
53
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
export * from './add.ts';
|
|
2
2
|
export * from './add-backend.ts';
|
|
3
|
+
export * from './agent.ts';
|
|
3
4
|
export * from './backend-inspect.ts';
|
|
4
5
|
export * from './build-plan.ts';
|
|
5
6
|
export * from './build.ts';
|
|
6
7
|
export * from './dev-server.ts';
|
|
8
|
+
export * from './doctor.ts';
|
|
7
9
|
export * from './enable.ts';
|
|
8
10
|
export * from './execute.ts';
|
|
9
11
|
export * from './format.ts';
|
|
12
|
+
export * from './frontend-inspect.ts';
|
|
10
13
|
export * from './init.ts';
|
|
14
|
+
export * from './inspect.ts';
|
|
15
|
+
export * from './mcp/run-cli-json.ts';
|
|
16
|
+
export * from './mcp/server.ts';
|
|
17
|
+
export * from './operations.ts';
|
|
11
18
|
export * from './publish.ts';
|
|
12
19
|
export * from './repair.ts';
|
|
13
20
|
export * from './refresh.ts';
|
package/src/init-assets.ts
CHANGED
|
@@ -22,37 +22,67 @@ export function getRootScaffoldAssets(): readonly ScaffoldAsset[] {
|
|
|
22
22
|
createAsset(sharedTemplateRoot, 'Errors.500.html', 'Errors.500.html'),
|
|
23
23
|
createAsset(sharedTemplateRoot, 'Errors.default.html', 'Errors.default.html'),
|
|
24
24
|
createAsset(sharedTemplateRoot, 'types.global.d.ts', 'types.global.d.ts'),
|
|
25
|
-
createAsset(
|
|
25
|
+
createAsset(
|
|
26
|
+
sharedTemplateRoot,
|
|
27
|
+
path.join('types', 'global.d.ts'),
|
|
28
|
+
path.join('types', 'global.d.ts'),
|
|
29
|
+
),
|
|
26
30
|
];
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
export async function getModeScaffoldAssets(
|
|
33
|
+
export async function getModeScaffoldAssets(
|
|
34
|
+
mode: WorkspaceMode,
|
|
35
|
+
): Promise<readonly ScaffoldAsset[]> {
|
|
30
36
|
switch (mode) {
|
|
31
37
|
case 'ssg':
|
|
32
38
|
return collectModeAssets([
|
|
33
|
-
{
|
|
39
|
+
{
|
|
40
|
+
sourceRoot: path.join(ssgTemplateRoot, 'src', 'frontend'),
|
|
41
|
+
targetRoot: path.join('src', 'frontend'),
|
|
42
|
+
},
|
|
34
43
|
]);
|
|
35
44
|
case 'spa':
|
|
36
45
|
return collectModeAssets([
|
|
37
|
-
{
|
|
38
|
-
|
|
46
|
+
{
|
|
47
|
+
sourceRoot: path.join(spaTemplateRoot, 'src', 'frontend'),
|
|
48
|
+
targetRoot: path.join('src', 'frontend'),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
sourceRoot: path.join(spaTemplateRoot, 'src', 'shared'),
|
|
52
|
+
targetRoot: path.join('src', 'shared'),
|
|
53
|
+
},
|
|
39
54
|
]);
|
|
40
55
|
case 'api':
|
|
41
56
|
return collectModeAssets([
|
|
42
|
-
{
|
|
43
|
-
|
|
57
|
+
{
|
|
58
|
+
sourceRoot: path.join(apiTemplateRoot, 'src', 'backend'),
|
|
59
|
+
targetRoot: path.join('src', 'backend'),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
sourceRoot: path.join(apiTemplateRoot, 'src', 'shared'),
|
|
63
|
+
targetRoot: path.join('src', 'shared'),
|
|
64
|
+
},
|
|
44
65
|
]);
|
|
45
66
|
case 'full':
|
|
46
67
|
return collectModeAssets([
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
{
|
|
69
|
+
sourceRoot: path.join(fullTemplateRoot, 'src', 'frontend'),
|
|
70
|
+
targetRoot: path.join('src', 'frontend'),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
sourceRoot: path.join(fullTemplateRoot, 'src', 'backend'),
|
|
74
|
+
targetRoot: path.join('src', 'backend'),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
sourceRoot: path.join(fullTemplateRoot, 'src', 'shared'),
|
|
78
|
+
targetRoot: path.join('src', 'shared'),
|
|
79
|
+
},
|
|
50
80
|
]);
|
|
51
81
|
}
|
|
52
82
|
}
|
|
53
83
|
|
|
54
84
|
async function collectModeAssets(
|
|
55
|
-
roots: readonly { sourceRoot: string; targetRoot: string }[]
|
|
85
|
+
roots: readonly { sourceRoot: string; targetRoot: string }[],
|
|
56
86
|
): Promise<readonly ScaffoldAsset[]> {
|
|
57
87
|
const assets: ScaffoldAsset[] = [];
|
|
58
88
|
for (const root of roots) {
|
package/src/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdir, readdir } from 'node:fs/promises';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -7,15 +7,7 @@ import { getModeScaffoldAssets, getRootScaffoldAssets } from './init-assets.ts';
|
|
|
7
7
|
import { monorepoRoot } from './paths.ts';
|
|
8
8
|
import type { WorkspaceMode } from './types.ts';
|
|
9
9
|
|
|
10
|
-
const PACKAGE_MANAGER = 'bun@1.3.
|
|
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;
|
|
10
|
+
const PACKAGE_MANAGER = 'bun@1.3.11';
|
|
19
11
|
|
|
20
12
|
const MODE_DESCRIPTIONS: Record<WorkspaceMode, string> = {
|
|
21
13
|
ssg: 'Static site (SSG) workspace for Webstir.',
|
|
@@ -37,15 +29,26 @@ export interface InitResult {
|
|
|
37
29
|
readonly changes: readonly string[];
|
|
38
30
|
}
|
|
39
31
|
|
|
32
|
+
interface ScaffoldMetadata {
|
|
33
|
+
readonly packageName?: string;
|
|
34
|
+
readonly description?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let repoWorkspacePatternsPromise: Promise<readonly string[]> | undefined;
|
|
38
|
+
|
|
40
39
|
export async function runInit(options: RunInitOptions): Promise<InitResult> {
|
|
41
|
-
const request = parseInitRequest(
|
|
40
|
+
const request = parseInitRequest(
|
|
41
|
+
options.args,
|
|
42
|
+
options.workspaceRoot,
|
|
43
|
+
options.cwd ?? process.cwd(),
|
|
44
|
+
);
|
|
42
45
|
return scaffoldWorkspace(request.mode, request.workspaceRoot, { force: false });
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export async function scaffoldWorkspace(
|
|
46
49
|
mode: WorkspaceMode,
|
|
47
50
|
workspaceRoot: string,
|
|
48
|
-
options: { readonly force: boolean }
|
|
51
|
+
options: { readonly force: boolean; readonly metadata?: ScaffoldMetadata },
|
|
49
52
|
): Promise<InitResult> {
|
|
50
53
|
if (existsSync(workspaceRoot) && !options.force && !(await isDirectoryEmpty(workspaceRoot))) {
|
|
51
54
|
throw new Error(`Refusing to initialize non-empty directory: ${workspaceRoot}`);
|
|
@@ -53,34 +56,31 @@ export async function scaffoldWorkspace(
|
|
|
53
56
|
|
|
54
57
|
await mkdir(workspaceRoot, { recursive: true });
|
|
55
58
|
|
|
56
|
-
const packageName = resolvePackageName(workspaceRoot);
|
|
59
|
+
const packageName = resolvePackageName(workspaceRoot, options.metadata);
|
|
57
60
|
const dependencySpecs = await resolveDependencySpecs(workspaceRoot);
|
|
58
61
|
const changes: string[] = [];
|
|
59
62
|
|
|
60
63
|
for (const asset of getRootScaffoldAssets()) {
|
|
61
64
|
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
62
|
-
await
|
|
63
|
-
await cp(asset.sourcePath, targetPath, { force: true });
|
|
65
|
+
await copyAsset(asset.sourcePath, targetPath);
|
|
64
66
|
changes.push(toWorkspaceRelative(workspaceRoot, targetPath));
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
for (const asset of await getModeScaffoldAssets(mode)) {
|
|
68
70
|
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
69
|
-
await
|
|
70
|
-
await cp(asset.sourcePath, targetPath, { force: true });
|
|
71
|
+
await copyAsset(asset.sourcePath, targetPath);
|
|
71
72
|
changes.push(toWorkspaceRelative(workspaceRoot, targetPath));
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
75
|
-
await
|
|
76
|
+
await Bun.write(
|
|
76
77
|
packageJsonPath,
|
|
77
|
-
`${JSON.stringify(createPackageJson(mode,
|
|
78
|
-
'utf8'
|
|
78
|
+
`${JSON.stringify(createPackageJson(mode, packageName, dependencySpecs, options.metadata), null, 2)}\n`,
|
|
79
79
|
);
|
|
80
80
|
changes.push('package.json');
|
|
81
81
|
|
|
82
82
|
const baseTsconfigPath = path.join(workspaceRoot, 'base.tsconfig.json');
|
|
83
|
-
await
|
|
83
|
+
await Bun.write(baseTsconfigPath, `${JSON.stringify(createBaseTsconfig(mode), null, 2)}\n`);
|
|
84
84
|
changes.push('base.tsconfig.json');
|
|
85
85
|
|
|
86
86
|
return {
|
|
@@ -94,13 +94,15 @@ export async function scaffoldWorkspace(
|
|
|
94
94
|
function parseInitRequest(
|
|
95
95
|
args: readonly string[],
|
|
96
96
|
workspaceOverride: string | undefined,
|
|
97
|
-
cwd: string
|
|
97
|
+
cwd: string,
|
|
98
98
|
): { readonly mode: WorkspaceMode; readonly workspaceRoot: string } {
|
|
99
99
|
const [firstArg, secondArg] = args;
|
|
100
100
|
|
|
101
101
|
if (workspaceOverride) {
|
|
102
102
|
if (!firstArg) {
|
|
103
|
-
throw new Error(
|
|
103
|
+
throw new Error(
|
|
104
|
+
'Usage: webstir init <mode> --workspace <path> or webstir init <mode> <directory>.',
|
|
105
|
+
);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
return {
|
|
@@ -128,7 +130,12 @@ function parseInitRequest(
|
|
|
128
130
|
|
|
129
131
|
function parseWorkspaceMode(value: string): WorkspaceMode {
|
|
130
132
|
const normalized = value.trim().toLowerCase();
|
|
131
|
-
if (
|
|
133
|
+
if (
|
|
134
|
+
normalized === 'ssg' ||
|
|
135
|
+
normalized === 'spa' ||
|
|
136
|
+
normalized === 'api' ||
|
|
137
|
+
normalized === 'full'
|
|
138
|
+
) {
|
|
132
139
|
return normalized;
|
|
133
140
|
}
|
|
134
141
|
|
|
@@ -149,7 +156,8 @@ async function isRepoWorkspacePath(workspaceRoot: string): Promise<boolean> {
|
|
|
149
156
|
return false;
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
|
|
159
|
+
const repoWorkspacePatterns = await readRepoWorkspacePatterns();
|
|
160
|
+
return repoWorkspacePatterns.some((pattern) => matchesWorkspacePattern(relative, pattern));
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
async function resolveDependencySpecs(workspaceRoot: string): Promise<Record<string, string>> {
|
|
@@ -162,14 +170,18 @@ async function resolveDependencySpecs(workspaceRoot: string): Promise<Record<str
|
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
return {
|
|
165
|
-
'@webstir-io/webstir-frontend': await readInstalledPackageVersion(
|
|
173
|
+
'@webstir-io/webstir-frontend': await readInstalledPackageVersion(
|
|
174
|
+
'@webstir-io/webstir-frontend',
|
|
175
|
+
),
|
|
166
176
|
'@webstir-io/webstir-backend': await readInstalledPackageVersion('@webstir-io/webstir-backend'),
|
|
167
177
|
'@webstir-io/webstir-testing': await readInstalledPackageVersion('@webstir-io/webstir-testing'),
|
|
168
178
|
};
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
async function readPackageVersion(packageJsonPath: string): Promise<string> {
|
|
172
|
-
const packageJson = JSON.parse(await
|
|
182
|
+
const packageJson = JSON.parse(await readTextFile(packageJsonPath)) as {
|
|
183
|
+
readonly version?: string;
|
|
184
|
+
};
|
|
173
185
|
if (!packageJson.version) {
|
|
174
186
|
throw new Error(`Missing version in ${packageJsonPath}`);
|
|
175
187
|
}
|
|
@@ -179,9 +191,9 @@ async function readPackageVersion(packageJsonPath: string): Promise<string> {
|
|
|
179
191
|
|
|
180
192
|
function createPackageJson(
|
|
181
193
|
mode: WorkspaceMode,
|
|
182
|
-
workspaceRoot: string,
|
|
183
194
|
packageName: string,
|
|
184
|
-
dependencySpecs: Record<string, string
|
|
195
|
+
dependencySpecs: Record<string, string>,
|
|
196
|
+
metadata: ScaffoldMetadata | undefined,
|
|
185
197
|
): Record<string, unknown> {
|
|
186
198
|
const dependencies: Record<string, string> = {
|
|
187
199
|
'@webstir-io/webstir-testing': dependencySpecs['@webstir-io/webstir-testing'],
|
|
@@ -200,7 +212,7 @@ function createPackageJson(
|
|
|
200
212
|
version: '1.0.0',
|
|
201
213
|
private: true,
|
|
202
214
|
type: 'module',
|
|
203
|
-
description:
|
|
215
|
+
description: metadata?.description ?? MODE_DESCRIPTIONS[mode],
|
|
204
216
|
dependencies,
|
|
205
217
|
devDependencies: {
|
|
206
218
|
'@types/node': '^20.0.0',
|
|
@@ -255,33 +267,15 @@ function createBaseTsconfig(mode: WorkspaceMode): Record<string, unknown> {
|
|
|
255
267
|
sourceMap: true,
|
|
256
268
|
declaration: false,
|
|
257
269
|
removeComments: true,
|
|
258
|
-
typeRoots: [
|
|
259
|
-
'./types',
|
|
260
|
-
'./node_modules/@types',
|
|
261
|
-
],
|
|
270
|
+
typeRoots: ['./types', './node_modules/@types'],
|
|
262
271
|
inlineSources: true,
|
|
263
272
|
},
|
|
264
273
|
};
|
|
265
274
|
}
|
|
266
275
|
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
276
|
+
function resolvePackageName(workspaceRoot: string, metadata: ScaffoldMetadata | undefined): string {
|
|
277
|
+
if (metadata?.packageName?.trim()) {
|
|
278
|
+
return metadata.packageName;
|
|
285
279
|
}
|
|
286
280
|
|
|
287
281
|
return sanitizePackageName(path.basename(workspaceRoot));
|
|
@@ -293,25 +287,12 @@ async function readInstalledPackageVersion(packageName: string): Promise<string>
|
|
|
293
287
|
return await readPackageVersion(packageJsonPath);
|
|
294
288
|
}
|
|
295
289
|
|
|
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
290
|
function sanitizePackageName(value: string): string {
|
|
314
|
-
const normalized = value
|
|
291
|
+
const normalized = value
|
|
292
|
+
.trim()
|
|
293
|
+
.toLowerCase()
|
|
294
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
295
|
+
.replace(/^-+|-+$/g, '');
|
|
315
296
|
return normalized || 'webstir-project';
|
|
316
297
|
}
|
|
317
298
|
|
|
@@ -327,7 +308,9 @@ function matchesWorkspacePattern(relativePath: string, pattern: string): boolean
|
|
|
327
308
|
return false;
|
|
328
309
|
}
|
|
329
310
|
|
|
330
|
-
return patternSegments.every(
|
|
311
|
+
return patternSegments.every(
|
|
312
|
+
(segment, index) => segment === '*' || segment === relativeSegments[index],
|
|
313
|
+
);
|
|
331
314
|
}
|
|
332
315
|
|
|
333
316
|
function uniqueSorted(values: readonly string[]): string[] {
|
|
@@ -337,3 +320,34 @@ function uniqueSorted(values: readonly string[]): string[] {
|
|
|
337
320
|
function toWorkspaceRelative(workspaceRoot: string, absolutePath: string): string {
|
|
338
321
|
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, '/');
|
|
339
322
|
}
|
|
323
|
+
|
|
324
|
+
async function readRepoWorkspacePatterns(): Promise<readonly string[]> {
|
|
325
|
+
if (!monorepoRoot) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
repoWorkspacePatternsPromise ??= loadRepoWorkspacePatterns();
|
|
330
|
+
return await repoWorkspacePatternsPromise;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function loadRepoWorkspacePatterns(): Promise<readonly string[]> {
|
|
334
|
+
if (!monorepoRoot) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const packageJsonPath = path.join(monorepoRoot, 'package.json');
|
|
339
|
+
const packageJson = JSON.parse(await readTextFile(packageJsonPath)) as {
|
|
340
|
+
readonly workspaces?: readonly string[];
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return packageJson.workspaces ?? [];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function copyAsset(sourcePath: string, targetPath: string): Promise<void> {
|
|
347
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
348
|
+
await Bun.write(targetPath, Bun.file(sourcePath));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function readTextFile(filePath: string): Promise<string> {
|
|
352
|
+
return await Bun.file(filePath).text();
|
|
353
|
+
}
|