@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.
- package/README.md +69 -0
- package/assets/features/client_nav/client_nav.ts +469 -0
- package/assets/features/content_nav/content_nav.css +170 -0
- package/assets/features/content_nav/content_nav.ts +358 -0
- package/assets/features/router/router-types.ts +6 -0
- package/assets/features/router/router.ts +118 -0
- package/assets/features/search/search.css +204 -0
- package/assets/features/search/search.ts +627 -0
- package/assets/templates/api/src/backend/index.ts +13 -0
- package/assets/templates/api/src/backend/tsconfig.json +15 -0
- package/assets/templates/api/src/shared/router-types.ts +23 -0
- package/assets/templates/api/src/shared/tsconfig.json +10 -0
- package/assets/templates/api/src/shared/types/index.ts +4 -0
- package/assets/templates/full/src/backend/index.ts +13 -0
- package/assets/templates/full/src/backend/tsconfig.json +15 -0
- package/assets/templates/full/src/frontend/app/app.css +65 -0
- package/assets/templates/full/src/frontend/app/app.html +13 -0
- package/assets/templates/full/src/frontend/app/app.ts +188 -0
- package/assets/templates/full/src/frontend/app/error.ts +127 -0
- package/assets/templates/full/src/frontend/app/hmr.js +355 -0
- package/assets/templates/full/src/frontend/app/navigation.ts +8 -0
- package/assets/templates/full/src/frontend/app/refresh.js +114 -0
- package/assets/templates/full/src/frontend/app/router.ts +126 -0
- package/assets/templates/full/src/frontend/app/styles/base.css +2 -0
- package/assets/templates/full/src/frontend/app/styles/reset.css +48 -0
- package/assets/templates/full/src/frontend/pages/home/index.css +21 -0
- package/assets/templates/full/src/frontend/pages/home/index.html +10 -0
- package/assets/templates/full/src/frontend/pages/home/index.ts +18 -0
- package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +21 -0
- package/assets/templates/full/src/frontend/tsconfig.json +20 -0
- package/assets/templates/full/src/shared/router-types.ts +23 -0
- package/assets/templates/full/src/shared/tsconfig.json +10 -0
- package/assets/templates/full/src/shared/types/index.ts +4 -0
- package/assets/templates/shared/Errors.404.html +23 -0
- package/assets/templates/shared/Errors.500.html +23 -0
- package/assets/templates/shared/Errors.default.html +23 -0
- package/assets/templates/shared/types/global.d.ts +32 -0
- package/assets/templates/shared/types.global.d.ts +32 -0
- package/assets/templates/spa/src/frontend/app/app.css +65 -0
- package/assets/templates/spa/src/frontend/app/app.html +13 -0
- package/assets/templates/spa/src/frontend/app/app.ts +188 -0
- package/assets/templates/spa/src/frontend/app/error.ts +127 -0
- package/assets/templates/spa/src/frontend/app/hmr.js +355 -0
- package/assets/templates/spa/src/frontend/app/navigation.ts +8 -0
- package/assets/templates/spa/src/frontend/app/refresh.js +114 -0
- package/assets/templates/spa/src/frontend/app/router.ts +126 -0
- package/assets/templates/spa/src/frontend/app/styles/base.css +2 -0
- package/assets/templates/spa/src/frontend/app/styles/reset.css +48 -0
- package/assets/templates/spa/src/frontend/pages/home/index.css +21 -0
- package/assets/templates/spa/src/frontend/pages/home/index.html +10 -0
- package/assets/templates/spa/src/frontend/pages/home/index.ts +18 -0
- package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +21 -0
- package/assets/templates/spa/src/frontend/tsconfig.json +20 -0
- package/assets/templates/spa/src/shared/router-types.ts +23 -0
- package/assets/templates/spa/src/shared/tsconfig.json +10 -0
- package/assets/templates/spa/src/shared/types/index.ts +4 -0
- package/assets/templates/ssg/src/frontend/app/app.css +12 -0
- package/assets/templates/ssg/src/frontend/app/app.html +43 -0
- package/assets/templates/ssg/src/frontend/app/app.ts +190 -0
- package/assets/templates/ssg/src/frontend/app/error.ts +127 -0
- package/assets/templates/ssg/src/frontend/app/hmr.js +370 -0
- package/assets/templates/ssg/src/frontend/app/refresh.js +163 -0
- package/assets/templates/ssg/src/frontend/app/scripts/components/drawer.ts +183 -0
- package/assets/templates/ssg/src/frontend/app/scripts/components/menu.ts +75 -0
- package/assets/templates/ssg/src/frontend/app/styles/base.css +77 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/buttons.css +108 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/drawer.css +12 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/header.css +164 -0
- package/assets/templates/ssg/src/frontend/app/styles/components/markdown.css +25 -0
- package/assets/templates/ssg/src/frontend/app/styles/layout.css +41 -0
- package/assets/templates/ssg/src/frontend/app/styles/reset.css +56 -0
- package/assets/templates/ssg/src/frontend/app/styles/tokens.css +72 -0
- package/assets/templates/ssg/src/frontend/app/styles/utilities.css +14 -0
- package/assets/templates/ssg/src/frontend/content/_sidebar.json +14 -0
- package/assets/templates/ssg/src/frontend/content/content-pipeline.md +82 -0
- package/assets/templates/ssg/src/frontend/content/css-playbook.md +68 -0
- package/assets/templates/ssg/src/frontend/content/hosting.md +48 -0
- package/assets/templates/ssg/src/frontend/pages/about/index.css +33 -0
- package/assets/templates/ssg/src/frontend/pages/about/index.html +60 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.css +505 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.html +52 -0
- package/assets/templates/ssg/src/frontend/pages/docs/index.ts +495 -0
- package/assets/templates/ssg/src/frontend/pages/home/index.css +91 -0
- package/assets/templates/ssg/src/frontend/pages/home/index.html +38 -0
- package/assets/templates/ssg/src/frontend/pages/home/tests/home.test.ts +24 -0
- package/assets/templates/ssg/src/frontend/tsconfig.json +13 -0
- package/package.json +41 -0
- package/scripts/pack-standalone.mjs +127 -0
- package/scripts/sync-assets.mjs +87 -0
- package/src/add-backend.ts +164 -0
- package/src/add.ts +112 -0
- package/src/api-watch.ts +84 -0
- package/src/backend-inspect.ts +45 -0
- package/src/backend-runtime.ts +286 -0
- package/src/build-plan.ts +12 -0
- package/src/build.ts +10 -0
- package/src/cli.ts +569 -0
- package/src/compile-tests.ts +61 -0
- package/src/dev-server.ts +393 -0
- package/src/enable-assets.ts +196 -0
- package/src/enable.ts +477 -0
- package/src/execute.ts +85 -0
- package/src/format.ts +254 -0
- package/src/frontend-watch.ts +145 -0
- package/src/full-watch.ts +80 -0
- package/src/index.ts +20 -0
- package/src/init-assets.ts +96 -0
- package/src/init.ts +339 -0
- package/src/paths.ts +26 -0
- package/src/providers.ts +88 -0
- package/src/publish.ts +8 -0
- package/src/refresh.ts +56 -0
- package/src/repair.ts +414 -0
- package/src/runtime.ts +48 -0
- package/src/smoke.ts +161 -0
- package/src/stop-signal.ts +26 -0
- package/src/test.ts +215 -0
- package/src/types.ts +29 -0
- package/src/watch-daemon-client.ts +171 -0
- package/src/watch-events.ts +195 -0
- package/src/watch.ts +66 -0
- package/src/workspace-watcher.ts +251 -0
- package/src/workspace.ts +55 -0
package/src/enable.ts
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { chmod, copyFile, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
import { getBackendScaffoldAssets } from '@webstir-io/webstir-backend';
|
|
6
|
+
import {
|
|
7
|
+
getClientNavAssets,
|
|
8
|
+
getContentNavAssets,
|
|
9
|
+
getSpaAssets,
|
|
10
|
+
getSearchAssets,
|
|
11
|
+
pageScriptTemplate,
|
|
12
|
+
renderGithubPagesDeployScript,
|
|
13
|
+
renderGithubPagesWorkflow,
|
|
14
|
+
type StaticFeatureAsset,
|
|
15
|
+
} from './enable-assets.ts';
|
|
16
|
+
import { readWorkspaceDescriptor } from './workspace.ts';
|
|
17
|
+
|
|
18
|
+
type EnableFeature =
|
|
19
|
+
| 'scripts'
|
|
20
|
+
| 'spa'
|
|
21
|
+
| 'client-nav'
|
|
22
|
+
| 'search'
|
|
23
|
+
| 'content-nav'
|
|
24
|
+
| 'backend'
|
|
25
|
+
| 'github-pages'
|
|
26
|
+
| 'gh-pages'
|
|
27
|
+
| 'gh-deploy';
|
|
28
|
+
|
|
29
|
+
export interface RunEnableOptions {
|
|
30
|
+
readonly workspaceRoot: string;
|
|
31
|
+
readonly args: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface EnableResult {
|
|
35
|
+
readonly workspaceRoot: string;
|
|
36
|
+
readonly feature: EnableFeature;
|
|
37
|
+
readonly changes: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runEnable(options: RunEnableOptions): Promise<EnableResult> {
|
|
41
|
+
const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
|
|
42
|
+
const [featureToken, ...rest] = options.args;
|
|
43
|
+
if (!featureToken) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'Missing enable feature. Usage: webstir enable <scripts <page>|spa|client-nav|search|content-nav|backend|github-pages|gh-deploy> --workspace <path>.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const feature = parseEnableFeature(featureToken);
|
|
50
|
+
const changes: string[] = [];
|
|
51
|
+
|
|
52
|
+
switch (feature) {
|
|
53
|
+
case 'scripts':
|
|
54
|
+
await enableScripts(workspace.root, rest, changes);
|
|
55
|
+
break;
|
|
56
|
+
case 'spa':
|
|
57
|
+
await copyStaticAssets(workspace.root, getSpaAssets(), changes);
|
|
58
|
+
await updatePackageJson(workspace.root, { enableSpa: true }, changes);
|
|
59
|
+
break;
|
|
60
|
+
case 'client-nav':
|
|
61
|
+
await copyStaticAssets(workspace.root, getClientNavAssets(), changes);
|
|
62
|
+
await ensureAppScriptImport(workspace.root, './scripts/features/client-nav.js', changes);
|
|
63
|
+
await updatePackageJson(workspace.root, { enableClientNav: true }, changes);
|
|
64
|
+
break;
|
|
65
|
+
case 'search':
|
|
66
|
+
await copyStaticAssets(workspace.root, getSearchAssets(), changes);
|
|
67
|
+
await ensureAppCssImport(workspace.root, './styles/features/search.css', changes);
|
|
68
|
+
await ensureHtmlSearchMode(workspace.root, changes);
|
|
69
|
+
await ensureAppScriptImport(workspace.root, './scripts/features/search.js', changes);
|
|
70
|
+
await updatePackageJson(workspace.root, { enableSearch: true }, changes);
|
|
71
|
+
break;
|
|
72
|
+
case 'content-nav':
|
|
73
|
+
await copyStaticAssets(workspace.root, getContentNavAssets(), changes);
|
|
74
|
+
await ensureAppCssImport(workspace.root, './styles/features/content-nav.css', changes);
|
|
75
|
+
await ensureAppScriptImport(workspace.root, './scripts/features/content-nav.js', changes);
|
|
76
|
+
await updatePackageJson(workspace.root, { enableContentNav: true }, changes);
|
|
77
|
+
break;
|
|
78
|
+
case 'backend':
|
|
79
|
+
await enableBackend(workspace.root, changes);
|
|
80
|
+
break;
|
|
81
|
+
case 'github-pages':
|
|
82
|
+
case 'gh-pages':
|
|
83
|
+
await enableGithubPages(workspace.root, path.basename(workspace.root), rest[0], false, changes);
|
|
84
|
+
break;
|
|
85
|
+
case 'gh-deploy':
|
|
86
|
+
await enableGithubPages(workspace.root, path.basename(workspace.root), rest[0], true, changes);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
workspaceRoot: workspace.root,
|
|
92
|
+
feature,
|
|
93
|
+
changes,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseEnableFeature(value: string): EnableFeature {
|
|
98
|
+
const normalized = value.trim().toLowerCase() as EnableFeature;
|
|
99
|
+
switch (normalized) {
|
|
100
|
+
case 'scripts':
|
|
101
|
+
case 'spa':
|
|
102
|
+
case 'client-nav':
|
|
103
|
+
case 'search':
|
|
104
|
+
case 'content-nav':
|
|
105
|
+
case 'backend':
|
|
106
|
+
case 'github-pages':
|
|
107
|
+
case 'gh-pages':
|
|
108
|
+
case 'gh-deploy':
|
|
109
|
+
return normalized;
|
|
110
|
+
default:
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Unknown feature "${value}". Expected scripts, spa, client-nav, search, content-nav, backend, github-pages, or gh-deploy.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function enableScripts(workspaceRoot: string, args: readonly string[], changes: string[]): Promise<void> {
|
|
118
|
+
const pageName = args[0];
|
|
119
|
+
if (!pageName) {
|
|
120
|
+
throw new Error('Usage: webstir enable scripts <page> --workspace <path>.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const pageDir = path.join(workspaceRoot, 'src', 'frontend', 'pages', pageName);
|
|
124
|
+
if (!existsSync(pageDir)) {
|
|
125
|
+
throw new Error(`Page "${pageName}" does not exist. Create it first.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const targetPath = path.join(pageDir, 'index.ts');
|
|
129
|
+
if (existsSync(targetPath)) {
|
|
130
|
+
throw new Error(`Page "${pageName}" already has an index.ts script.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await writeTextFile(targetPath, pageScriptTemplate);
|
|
134
|
+
changes.push(relativeWorkspacePath(workspaceRoot, targetPath));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function copyStaticAssets(
|
|
138
|
+
workspaceRoot: string,
|
|
139
|
+
assets: readonly StaticFeatureAsset[],
|
|
140
|
+
changes: string[]
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
for (const asset of assets) {
|
|
143
|
+
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
144
|
+
const sourceStats = await stat(asset.sourcePath);
|
|
145
|
+
if (!sourceStats.isFile()) {
|
|
146
|
+
throw new Error(`Feature asset not found: ${asset.sourcePath}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
150
|
+
if (!asset.overwrite && existsSync(targetPath)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await copyFile(asset.sourcePath, targetPath);
|
|
155
|
+
if (asset.executable) {
|
|
156
|
+
await chmod(targetPath, 0o755);
|
|
157
|
+
}
|
|
158
|
+
changes.push(relativeWorkspacePath(workspaceRoot, targetPath));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function enableBackend(workspaceRoot: string, changes: string[]): Promise<void> {
|
|
163
|
+
const backendRoot = path.join(workspaceRoot, 'src', 'backend');
|
|
164
|
+
if (!existsSync(backendRoot)) {
|
|
165
|
+
const assets = await getBackendScaffoldAssets();
|
|
166
|
+
for (const asset of assets) {
|
|
167
|
+
const targetPath = path.join(workspaceRoot, asset.targetPath);
|
|
168
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
169
|
+
await copyFile(asset.sourcePath, targetPath);
|
|
170
|
+
changes.push(relativeWorkspacePath(workspaceRoot, targetPath));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await updatePackageJson(workspaceRoot, { enableBackend: true, mode: 'full' }, changes);
|
|
175
|
+
await ensureTsReference(workspaceRoot, 'src/backend', changes);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function enableGithubPages(
|
|
179
|
+
workspaceRoot: string,
|
|
180
|
+
workspaceName: string,
|
|
181
|
+
basePathArg: string | undefined,
|
|
182
|
+
includeWorkflow: boolean,
|
|
183
|
+
changes: string[]
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
const resolvedBasePath = resolveGithubPagesBasePath(basePathArg, workspaceName);
|
|
186
|
+
const deployScriptPath = path.join(workspaceRoot, 'utils', 'deploy-gh-pages.sh');
|
|
187
|
+
await writeTextFile(deployScriptPath, renderGithubPagesDeployScript(), 0o755);
|
|
188
|
+
changes.push(relativeWorkspacePath(workspaceRoot, deployScriptPath));
|
|
189
|
+
|
|
190
|
+
if (includeWorkflow) {
|
|
191
|
+
const workflowPath = path.join(workspaceRoot, '.github', 'workflows', 'webstir-gh-pages.yml');
|
|
192
|
+
if (!existsSync(workflowPath)) {
|
|
193
|
+
await writeTextFile(workflowPath, renderGithubPagesWorkflow());
|
|
194
|
+
changes.push(relativeWorkspacePath(workspaceRoot, workflowPath));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await updateFrontendConfig(workspaceRoot, resolvedBasePath, changes);
|
|
199
|
+
await updatePackageJson(workspaceRoot, { enableGithubPages: true, ensureDeployScript: true }, changes);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function ensureAppScriptImport(
|
|
203
|
+
workspaceRoot: string,
|
|
204
|
+
importPath: string,
|
|
205
|
+
changes: string[]
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
const appTsPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.ts');
|
|
208
|
+
if (!existsSync(appTsPath)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const source = await readFile(appTsPath, 'utf8');
|
|
213
|
+
const updated = ensureSideEffectImport(source, importPath);
|
|
214
|
+
if (updated === source) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await writeFile(appTsPath, updated, 'utf8');
|
|
219
|
+
changes.push(relativeWorkspacePath(workspaceRoot, appTsPath));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function ensureAppCssImport(
|
|
223
|
+
workspaceRoot: string,
|
|
224
|
+
importPath: string,
|
|
225
|
+
changes: string[]
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
const appCssPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.css');
|
|
228
|
+
if (!existsSync(appCssPath)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const source = await readFile(appCssPath, 'utf8');
|
|
233
|
+
let updated = source;
|
|
234
|
+
updated = ensureLayerIncludes(updated, 'features');
|
|
235
|
+
updated = ensureImportIncludes(updated, importPath, './styles/components/buttons.css');
|
|
236
|
+
if (updated === source) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await writeFile(appCssPath, updated, 'utf8');
|
|
241
|
+
changes.push(relativeWorkspacePath(workspaceRoot, appCssPath));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function ensureHtmlSearchMode(workspaceRoot: string, changes: string[]): Promise<void> {
|
|
245
|
+
const appHtmlPath = path.join(workspaceRoot, 'src', 'frontend', 'app', 'app.html');
|
|
246
|
+
if (!existsSync(appHtmlPath)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const source = await readFile(appHtmlPath, 'utf8');
|
|
251
|
+
if (source.includes('data-webstir-search-styles=')) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const updated = source.replace(
|
|
256
|
+
/<html\b(?![^>]*\bdata-webstir-search-styles=)/i,
|
|
257
|
+
'<html data-webstir-search-styles="css"'
|
|
258
|
+
);
|
|
259
|
+
if (updated === source) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await writeFile(appHtmlPath, updated, 'utf8');
|
|
264
|
+
changes.push(relativeWorkspacePath(workspaceRoot, appHtmlPath));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function updatePackageJson(
|
|
268
|
+
workspaceRoot: string,
|
|
269
|
+
options: {
|
|
270
|
+
readonly enableSpa?: boolean;
|
|
271
|
+
readonly enableClientNav?: boolean;
|
|
272
|
+
readonly enableSearch?: boolean;
|
|
273
|
+
readonly enableContentNav?: boolean;
|
|
274
|
+
readonly enableBackend?: boolean;
|
|
275
|
+
readonly enableGithubPages?: boolean;
|
|
276
|
+
readonly mode?: string;
|
|
277
|
+
readonly ensureDeployScript?: boolean;
|
|
278
|
+
},
|
|
279
|
+
changes: string[]
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
const packageJsonPath = path.join(workspaceRoot, 'package.json');
|
|
282
|
+
const source = await readFile(packageJsonPath, 'utf8');
|
|
283
|
+
const root = JSON.parse(source) as Record<string, unknown>;
|
|
284
|
+
const webstir = asRecord(root.webstir);
|
|
285
|
+
const enable = asRecord(webstir.enable);
|
|
286
|
+
|
|
287
|
+
if (options.mode) {
|
|
288
|
+
webstir.mode = options.mode;
|
|
289
|
+
}
|
|
290
|
+
if (options.enableSpa !== undefined) {
|
|
291
|
+
enable.spa = options.enableSpa;
|
|
292
|
+
}
|
|
293
|
+
if (options.enableClientNav !== undefined) {
|
|
294
|
+
enable.clientNav = options.enableClientNav;
|
|
295
|
+
}
|
|
296
|
+
if (options.enableSearch !== undefined) {
|
|
297
|
+
enable.search = options.enableSearch;
|
|
298
|
+
}
|
|
299
|
+
if (options.enableContentNav !== undefined) {
|
|
300
|
+
enable.contentNav = options.enableContentNav;
|
|
301
|
+
}
|
|
302
|
+
if (options.enableBackend !== undefined) {
|
|
303
|
+
enable.backend = options.enableBackend;
|
|
304
|
+
}
|
|
305
|
+
if (options.enableGithubPages !== undefined) {
|
|
306
|
+
enable.githubPages = options.enableGithubPages;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
webstir.enable = enable;
|
|
310
|
+
root.webstir = webstir;
|
|
311
|
+
|
|
312
|
+
if (options.ensureDeployScript) {
|
|
313
|
+
const scripts = asRecord(root.scripts);
|
|
314
|
+
if (typeof scripts.deploy !== 'string') {
|
|
315
|
+
scripts.deploy = 'bash ./utils/deploy-gh-pages.sh';
|
|
316
|
+
}
|
|
317
|
+
root.scripts = scripts;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const updated = `${JSON.stringify(root, null, 2)}\n`;
|
|
321
|
+
if (updated === source) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await writeFile(packageJsonPath, updated, 'utf8');
|
|
326
|
+
changes.push(relativeWorkspacePath(workspaceRoot, packageJsonPath));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function updateFrontendConfig(workspaceRoot: string, basePath: string, changes: string[]): Promise<void> {
|
|
330
|
+
const configPath = path.join(workspaceRoot, 'src', 'frontend', 'frontend.config.json');
|
|
331
|
+
let root: Record<string, unknown> = {};
|
|
332
|
+
|
|
333
|
+
if (existsSync(configPath)) {
|
|
334
|
+
try {
|
|
335
|
+
root = JSON.parse(await readFile(configPath, 'utf8')) as Record<string, unknown>;
|
|
336
|
+
} catch {
|
|
337
|
+
root = {};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const publish = asRecord(root.publish);
|
|
342
|
+
publish.basePath = basePath;
|
|
343
|
+
root.publish = publish;
|
|
344
|
+
|
|
345
|
+
const updated = `${JSON.stringify(root, null, 2)}\n`;
|
|
346
|
+
const current = existsSync(configPath) ? await readFile(configPath, 'utf8') : null;
|
|
347
|
+
if (current === updated) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await writeTextFile(configPath, updated);
|
|
352
|
+
changes.push(relativeWorkspacePath(workspaceRoot, configPath));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function ensureTsReference(workspaceRoot: string, referencePath: string, changes: string[]): Promise<void> {
|
|
356
|
+
const tsconfigPath = path.join(workspaceRoot, 'base.tsconfig.json');
|
|
357
|
+
if (!existsSync(tsconfigPath)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const source = await readFile(tsconfigPath, 'utf8');
|
|
362
|
+
const root = JSON.parse(source) as Record<string, unknown>;
|
|
363
|
+
const references = Array.isArray(root.references) ? [...root.references] : [];
|
|
364
|
+
const exists = references.some((entry) =>
|
|
365
|
+
typeof entry === 'object'
|
|
366
|
+
&& entry !== null
|
|
367
|
+
&& (entry as Record<string, unknown>).path === referencePath
|
|
368
|
+
);
|
|
369
|
+
if (!exists) {
|
|
370
|
+
references.push({ path: referencePath });
|
|
371
|
+
}
|
|
372
|
+
root.references = references;
|
|
373
|
+
|
|
374
|
+
const updated = `${JSON.stringify(root, null, 2)}\n`;
|
|
375
|
+
if (updated === source) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await writeFile(tsconfigPath, updated, 'utf8');
|
|
380
|
+
changes.push(relativeWorkspacePath(workspaceRoot, tsconfigPath));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function resolveGithubPagesBasePath(basePathArg: string | undefined, workspaceName: string): string {
|
|
384
|
+
const candidate = (basePathArg ?? workspaceName).trim();
|
|
385
|
+
if (!candidate || candidate === '/') {
|
|
386
|
+
return '/';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const withLeadingSlash = candidate.startsWith('/') ? candidate : `/${candidate}`;
|
|
390
|
+
return withLeadingSlash.length > 1 && withLeadingSlash.endsWith('/')
|
|
391
|
+
? withLeadingSlash.slice(0, -1)
|
|
392
|
+
: withLeadingSlash;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function ensureSideEffectImport(source: string, importPath: string): string {
|
|
396
|
+
const escaped = escapeRegExp(importPath);
|
|
397
|
+
const pattern = new RegExp(`^\\s*import\\s+(['"])${escaped}\\1\\s*;?\\s*$`, 'm');
|
|
398
|
+
if (pattern.test(source)) {
|
|
399
|
+
return source;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const suffix = source.endsWith('\n') ? '' : '\n';
|
|
403
|
+
return `${source}${suffix}import "${importPath}";\n`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function ensureLayerIncludes(css: string, layerName: string): string {
|
|
407
|
+
const match = css.match(/@layer\s+([^;]+);/);
|
|
408
|
+
if (!match || match.index === undefined) {
|
|
409
|
+
return css;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const layers = match[1].split(',').map((layer) => layer.trim()).filter(Boolean);
|
|
413
|
+
if (layers.includes(layerName)) {
|
|
414
|
+
return css;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const updated = [...layers];
|
|
418
|
+
const utilitiesIndex = updated.indexOf('utilities');
|
|
419
|
+
const overridesIndex = updated.indexOf('overrides');
|
|
420
|
+
const insertIndex = utilitiesIndex >= 0 ? utilitiesIndex : overridesIndex >= 0 ? overridesIndex : updated.length;
|
|
421
|
+
updated.splice(insertIndex, 0, layerName);
|
|
422
|
+
const replacement = `@layer ${updated.join(', ')};`;
|
|
423
|
+
return `${css.slice(0, match.index)}${replacement}${css.slice(match.index + match[0].length)}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function ensureImportIncludes(css: string, importPath: string, insertAfterImportPath: string): string {
|
|
427
|
+
if (css.includes(`@import "${importPath}"`) || css.includes(`@import '${importPath}'`)) {
|
|
428
|
+
return css;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const doubleNeedle = `@import "${insertAfterImportPath}"`;
|
|
432
|
+
const singleNeedle = `@import '${insertAfterImportPath}'`;
|
|
433
|
+
let insertAfterIndex = css.indexOf(doubleNeedle);
|
|
434
|
+
let needle = doubleNeedle;
|
|
435
|
+
if (insertAfterIndex < 0) {
|
|
436
|
+
insertAfterIndex = css.indexOf(singleNeedle);
|
|
437
|
+
needle = singleNeedle;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (insertAfterIndex >= 0) {
|
|
441
|
+
const lineEnd = css.indexOf('\n', insertAfterIndex);
|
|
442
|
+
const insertAt = lineEnd >= 0 ? lineEnd + 1 : css.length;
|
|
443
|
+
return `${css.slice(0, insertAt)}@import "${importPath}";\n${css.slice(insertAt)}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const matches = [...css.matchAll(/@import\s+['"][^'"]+['"];?/g)];
|
|
447
|
+
if (matches.length > 0) {
|
|
448
|
+
const last = matches[matches.length - 1];
|
|
449
|
+
const insertAt = (last.index ?? 0) + last[0].length;
|
|
450
|
+
const separator = css[insertAt] === '\n' ? '' : '\n';
|
|
451
|
+
return `${css.slice(0, insertAt)}${separator}@import "${importPath}";\n${css.slice(insertAt)}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return `${css}\n@import "${importPath}";\n`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function writeTextFile(filePath: string, contents: string, mode?: number): Promise<void> {
|
|
458
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
459
|
+
await writeFile(filePath, contents, 'utf8');
|
|
460
|
+
if (mode !== undefined) {
|
|
461
|
+
await chmod(filePath, mode);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
466
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
467
|
+
? { ...(value as Record<string, unknown>) }
|
|
468
|
+
: {};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function relativeWorkspacePath(workspaceRoot: string, absolutePath: string): string {
|
|
472
|
+
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, '/');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function escapeRegExp(value: string): string {
|
|
476
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
477
|
+
}
|
package/src/execute.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { createBuildPlan } from './build-plan.ts';
|
|
4
|
+
import { loadProvider } from './providers.ts';
|
|
5
|
+
import { createWorkspaceRuntimeEnv } from './runtime.ts';
|
|
6
|
+
import type {
|
|
7
|
+
BuildProvider,
|
|
8
|
+
BuildTargetKind,
|
|
9
|
+
CommandExecutionResult,
|
|
10
|
+
CommandMode,
|
|
11
|
+
} from './types.ts';
|
|
12
|
+
import { readWorkspaceDescriptor } from './workspace.ts';
|
|
13
|
+
|
|
14
|
+
export interface RunCommandOptions {
|
|
15
|
+
readonly workspaceRoot: string;
|
|
16
|
+
readonly env?: Record<string, string | undefined>;
|
|
17
|
+
readonly loadProvider?: (kind: BuildTargetKind) => Promise<BuildProvider>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runCommand(
|
|
21
|
+
mode: CommandMode,
|
|
22
|
+
options: RunCommandOptions
|
|
23
|
+
): Promise<CommandExecutionResult> {
|
|
24
|
+
const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
|
|
25
|
+
const providerLoader = options.loadProvider ?? loadProvider;
|
|
26
|
+
const targets = [];
|
|
27
|
+
|
|
28
|
+
for (const kind of createBuildPlan(workspace.mode)) {
|
|
29
|
+
const provider = await providerLoader(kind);
|
|
30
|
+
const resolvedWorkspace = await provider.resolveWorkspace({
|
|
31
|
+
workspaceRoot: workspace.root,
|
|
32
|
+
config: {},
|
|
33
|
+
});
|
|
34
|
+
await prepareCommandTarget(provider, workspace.root, kind, mode, options.env);
|
|
35
|
+
const result = await provider.build({
|
|
36
|
+
workspaceRoot: workspace.root,
|
|
37
|
+
env: createWorkspaceRuntimeEnv(workspace.root, mode, options.env),
|
|
38
|
+
incremental: false,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
targets.push({
|
|
42
|
+
kind,
|
|
43
|
+
outputRoot: resolveOutputRoot(workspace.root, kind, mode, resolvedWorkspace.buildRoot),
|
|
44
|
+
result,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
mode,
|
|
50
|
+
workspace,
|
|
51
|
+
targets,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function prepareCommandTarget(
|
|
56
|
+
provider: BuildProvider,
|
|
57
|
+
workspaceRoot: string,
|
|
58
|
+
kind: BuildTargetKind,
|
|
59
|
+
mode: CommandMode,
|
|
60
|
+
env?: Record<string, string | undefined>
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
if (kind !== 'frontend' || mode !== 'publish') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Frontend publish consumes build/frontend artifacts while generating dist output.
|
|
67
|
+
await provider.build({
|
|
68
|
+
workspaceRoot,
|
|
69
|
+
env: createWorkspaceRuntimeEnv(workspaceRoot, 'build', env),
|
|
70
|
+
incremental: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveOutputRoot(
|
|
75
|
+
workspaceRoot: string,
|
|
76
|
+
kind: BuildTargetKind,
|
|
77
|
+
mode: CommandMode,
|
|
78
|
+
buildRoot: string
|
|
79
|
+
): string {
|
|
80
|
+
if (kind === 'frontend' && mode === 'publish') {
|
|
81
|
+
return path.join(workspaceRoot, 'dist', 'frontend');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return buildRoot;
|
|
85
|
+
}
|