@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/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webstir-io/webstir",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"webstir": "src/cli.ts"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "node scripts/sync-assets.mjs && bun run build:deps && tsc -p tsconfig.json",
|
|
10
|
+
"build:deps": "bun run --filter @webstir-io/module-contract build && bun run --filter @webstir-io/testing-contract build && bun run --filter @webstir-io/webstir-frontend build && bun run --filter @webstir-io/webstir-backend build && bun run --filter @webstir-io/webstir-testing build",
|
|
11
|
+
"test": "bun run build && bun test ./tests/*.ts ./tests/**/*.ts",
|
|
12
|
+
"clean": "rm -rf dist",
|
|
13
|
+
"prepack": "node scripts/sync-assets.mjs",
|
|
14
|
+
"pack:local": "bun pm pack",
|
|
15
|
+
"pack:standalone": "node scripts/pack-standalone.mjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"assets",
|
|
19
|
+
"scripts",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"bun": ">=1.3.5"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"registry": "https://registry.npmjs.org",
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@webstir-io/module-contract": "^0.1.14",
|
|
32
|
+
"@webstir-io/testing-contract": "^0.1.7",
|
|
33
|
+
"@webstir-io/webstir-backend": "^0.1.15",
|
|
34
|
+
"@webstir-io/webstir-frontend": "^0.1.40",
|
|
35
|
+
"@webstir-io/webstir-testing": "^0.1.5",
|
|
36
|
+
"typescript": "^5.7.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.19.21"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
7
|
+
|
|
8
|
+
const scriptRoot = path.dirname(new URL(import.meta.url).pathname);
|
|
9
|
+
const packageRoot = path.resolve(scriptRoot, '..');
|
|
10
|
+
const repoRoot = path.resolve(packageRoot, '..', '..');
|
|
11
|
+
const outputRoot = path.join(packageRoot, 'artifacts');
|
|
12
|
+
|
|
13
|
+
const internalPackages = [
|
|
14
|
+
['@webstir-io/module-contract', path.join(repoRoot, 'packages', 'contracts', 'module-contract')],
|
|
15
|
+
['@webstir-io/testing-contract', path.join(repoRoot, 'packages', 'contracts', 'testing-contract')],
|
|
16
|
+
['@webstir-io/webstir-frontend', path.join(repoRoot, 'packages', 'tooling', 'webstir-frontend')],
|
|
17
|
+
['@webstir-io/webstir-backend', path.join(repoRoot, 'packages', 'tooling', 'webstir-backend')],
|
|
18
|
+
['@webstir-io/webstir-testing', path.join(repoRoot, 'packages', 'tooling', 'webstir-testing')],
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
run('bun', ['run', 'build:deps'], packageRoot);
|
|
23
|
+
run('node', ['scripts/sync-assets.mjs'], packageRoot);
|
|
24
|
+
|
|
25
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'webstir-standalone-pack-'));
|
|
26
|
+
const tarballRoot = path.join(tempRoot, 'tarballs');
|
|
27
|
+
const stageRoot = path.join(tempRoot, 'stage');
|
|
28
|
+
|
|
29
|
+
await mkdir(tarballRoot, { recursive: true });
|
|
30
|
+
await mkdir(stageRoot, { recursive: true });
|
|
31
|
+
await mkdir(outputRoot, { recursive: true });
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const tarballs = new Map();
|
|
35
|
+
for (const [name, cwd] of internalPackages) {
|
|
36
|
+
tarballs.set(name, await packPackage(cwd, tarballRoot));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await cp(path.join(packageRoot, 'assets'), path.join(stageRoot, 'assets'), { recursive: true });
|
|
40
|
+
await cp(path.join(packageRoot, 'src'), path.join(stageRoot, 'src'), { recursive: true });
|
|
41
|
+
await cp(path.join(packageRoot, 'README.md'), path.join(stageRoot, 'README.md'));
|
|
42
|
+
|
|
43
|
+
const packageJson = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
44
|
+
const originalDependencies = { ...packageJson.dependencies };
|
|
45
|
+
packageJson.dependencies = Object.fromEntries(
|
|
46
|
+
Object.entries(originalDependencies).map(([name, version]) => {
|
|
47
|
+
const tarballPath = tarballs.get(name);
|
|
48
|
+
if (!tarballPath) {
|
|
49
|
+
return [name, version];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return [name, `file:${path.relative(stageRoot, tarballPath).split(path.sep).join('/')}`];
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
packageJson.bundledDependencies = Object.keys(packageJson.dependencies);
|
|
56
|
+
packageJson.files = ['assets', 'src', 'README.md'];
|
|
57
|
+
delete packageJson.scripts;
|
|
58
|
+
delete packageJson.devDependencies;
|
|
59
|
+
|
|
60
|
+
await writeFile(path.join(stageRoot, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
|
61
|
+
|
|
62
|
+
run('bun', ['install'], stageRoot);
|
|
63
|
+
packageJson.dependencies = originalDependencies;
|
|
64
|
+
await writeFile(path.join(stageRoot, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
|
65
|
+
const tarballName = run('bun', ['pm', 'pack'], stageRoot)
|
|
66
|
+
.split(/\r?\n/)
|
|
67
|
+
.map((line) => line.trim())
|
|
68
|
+
.filter((line) => line.endsWith('.tgz'))
|
|
69
|
+
.at(-1);
|
|
70
|
+
|
|
71
|
+
if (!tarballName) {
|
|
72
|
+
throw new Error('Failed to locate standalone tarball output.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const standaloneName = tarballName.replace(/\.tgz$/, '-standalone.tgz');
|
|
76
|
+
const destination = path.join(outputRoot, standaloneName);
|
|
77
|
+
await rm(destination, { force: true });
|
|
78
|
+
await rename(path.join(stageRoot, tarballName), destination);
|
|
79
|
+
console.log(destination);
|
|
80
|
+
} finally {
|
|
81
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function packPackage(cwd, outputDir) {
|
|
86
|
+
const tarballName = run('bun', ['pm', 'pack'], cwd)
|
|
87
|
+
.split(/\r?\n/)
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter((line) => line.endsWith('.tgz'))
|
|
90
|
+
.at(-1);
|
|
91
|
+
|
|
92
|
+
if (!tarballName) {
|
|
93
|
+
throw new Error(`Failed to pack ${cwd}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const source = path.join(cwd, tarballName);
|
|
97
|
+
const destination = path.join(outputDir, tarballName);
|
|
98
|
+
await rename(source, destination);
|
|
99
|
+
return destination;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function run(command, args, cwd) {
|
|
103
|
+
const result = spawnSync(command, args, {
|
|
104
|
+
cwd,
|
|
105
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
env: process.env,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (result.stdout) {
|
|
111
|
+
process.stdout.write(result.stdout);
|
|
112
|
+
}
|
|
113
|
+
if (result.stderr) {
|
|
114
|
+
process.stderr.write(result.stderr);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (result.status !== 0) {
|
|
118
|
+
throw new Error(`Command failed (${result.status ?? 'unknown'}): ${command} ${args.join(' ')}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result.stdout ?? '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch((error) => {
|
|
125
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cp, mkdir, rm } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const packageRoot = path.resolve(here, '..');
|
|
9
|
+
const repoRoot = path.resolve(packageRoot, '..', '..');
|
|
10
|
+
const assetsRoot = path.join(packageRoot, 'assets');
|
|
11
|
+
const templatesRoot = path.join(assetsRoot, 'templates');
|
|
12
|
+
const featuresRoot = path.join(assetsRoot, 'features');
|
|
13
|
+
|
|
14
|
+
const rootAssets = [
|
|
15
|
+
'Errors.404.html',
|
|
16
|
+
'Errors.500.html',
|
|
17
|
+
'Errors.default.html',
|
|
18
|
+
'types.global.d.ts',
|
|
19
|
+
path.join('types', 'global.d.ts'),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const modeTemplates = [
|
|
23
|
+
{
|
|
24
|
+
mode: 'ssg',
|
|
25
|
+
roots: [
|
|
26
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'ssg', 'base', 'src', 'frontend'), target: path.join('src', 'frontend') },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
mode: 'spa',
|
|
31
|
+
roots: [
|
|
32
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'spa', 'src', 'frontend'), target: path.join('src', 'frontend') },
|
|
33
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'spa', 'src', 'shared'), target: path.join('src', 'shared') },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
mode: 'api',
|
|
38
|
+
roots: [
|
|
39
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'api', 'src', 'backend'), target: path.join('src', 'backend') },
|
|
40
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'api', 'src', 'shared'), target: path.join('src', 'shared') },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
mode: 'full',
|
|
45
|
+
roots: [
|
|
46
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'full', 'src', 'frontend'), target: path.join('src', 'frontend') },
|
|
47
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'full', 'src', 'backend'), target: path.join('src', 'backend') },
|
|
48
|
+
{ source: path.join(repoRoot, 'examples', 'demos', 'full', 'src', 'shared'), target: path.join('src', 'shared') },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const features = [
|
|
54
|
+
{ source: path.join(repoRoot, 'orchestrators', 'dotnet', 'Engine', 'Resources', 'features', 'router'), target: 'router' },
|
|
55
|
+
{ source: path.join(repoRoot, 'orchestrators', 'dotnet', 'Engine', 'Resources', 'features', 'client_nav'), target: 'client_nav' },
|
|
56
|
+
{ source: path.join(repoRoot, 'orchestrators', 'dotnet', 'Engine', 'Resources', 'features', 'search'), target: 'search' },
|
|
57
|
+
{ source: path.join(repoRoot, 'orchestrators', 'dotnet', 'Engine', 'Resources', 'features', 'content_nav'), target: 'content_nav' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
await rm(assetsRoot, { recursive: true, force: true });
|
|
62
|
+
await mkdir(templatesRoot, { recursive: true });
|
|
63
|
+
await mkdir(featuresRoot, { recursive: true });
|
|
64
|
+
|
|
65
|
+
for (const relativePath of rootAssets) {
|
|
66
|
+
const sourcePath = path.join(repoRoot, 'examples', 'demos', 'spa', relativePath);
|
|
67
|
+
const targetPath = path.join(templatesRoot, 'shared', relativePath);
|
|
68
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
69
|
+
await cp(sourcePath, targetPath, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const template of modeTemplates) {
|
|
73
|
+
for (const root of template.roots) {
|
|
74
|
+
const targetPath = path.join(templatesRoot, template.mode, root.target);
|
|
75
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
76
|
+
await cp(root.source, targetPath, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const feature of features) {
|
|
81
|
+
const targetPath = path.join(featuresRoot, feature.target);
|
|
82
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
83
|
+
await cp(feature.source, targetPath, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await main();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { AddCommandResult } from './add.ts';
|
|
2
|
+
|
|
3
|
+
import { runAddJob, runAddRoute } from '@webstir-io/webstir-backend';
|
|
4
|
+
|
|
5
|
+
export interface RunAddBackendOptions {
|
|
6
|
+
readonly workspaceRoot: string;
|
|
7
|
+
readonly rawArgs: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function runAddRouteCommand(options: RunAddBackendOptions): Promise<AddCommandResult> {
|
|
11
|
+
const parsed = parseBackendCommandArgs(options.rawArgs, {
|
|
12
|
+
valueFlags: new Set([
|
|
13
|
+
'--method',
|
|
14
|
+
'--path',
|
|
15
|
+
'--summary',
|
|
16
|
+
'--description',
|
|
17
|
+
'--tags',
|
|
18
|
+
'--params-schema',
|
|
19
|
+
'--query-schema',
|
|
20
|
+
'--body-schema',
|
|
21
|
+
'--headers-schema',
|
|
22
|
+
'--response-schema',
|
|
23
|
+
'--response-status',
|
|
24
|
+
'--response-headers-schema',
|
|
25
|
+
]),
|
|
26
|
+
booleanFlags: new Set(['--fastify']),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const name = parsed.positionals[0];
|
|
30
|
+
if (!name) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Usage: webstir add-route <name> --workspace <path> [--method <METHOD>] [--path <path>] [--fastify].'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tags = parsed.values.get('--tags')
|
|
37
|
+
?.split(',')
|
|
38
|
+
.map((tag) => tag.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
|
|
41
|
+
const result = await runAddRoute({
|
|
42
|
+
workspaceRoot: options.workspaceRoot,
|
|
43
|
+
name,
|
|
44
|
+
method: parsed.values.get('--method'),
|
|
45
|
+
path: parsed.values.get('--path'),
|
|
46
|
+
fastify: parsed.booleans.has('--fastify'),
|
|
47
|
+
summary: parsed.values.get('--summary'),
|
|
48
|
+
description: parsed.values.get('--description'),
|
|
49
|
+
tags,
|
|
50
|
+
paramsSchema: parsed.values.get('--params-schema'),
|
|
51
|
+
querySchema: parsed.values.get('--query-schema'),
|
|
52
|
+
bodySchema: parsed.values.get('--body-schema'),
|
|
53
|
+
headersSchema: parsed.values.get('--headers-schema'),
|
|
54
|
+
responseSchema: parsed.values.get('--response-schema'),
|
|
55
|
+
responseStatus: parsed.values.get('--response-status'),
|
|
56
|
+
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
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runAddJobCommand(options: RunAddBackendOptions): Promise<AddCommandResult> {
|
|
68
|
+
const parsed = parseBackendCommandArgs(options.rawArgs, {
|
|
69
|
+
valueFlags: new Set(['--schedule', '--description', '--priority']),
|
|
70
|
+
booleanFlags: new Set(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const name = parsed.positionals[0];
|
|
74
|
+
if (!name) {
|
|
75
|
+
throw new Error('Usage: webstir add-job <name> --workspace <path> [--schedule <expression>].');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await runAddJob({
|
|
79
|
+
workspaceRoot: options.workspaceRoot,
|
|
80
|
+
name,
|
|
81
|
+
schedule: parsed.values.get('--schedule'),
|
|
82
|
+
description: parsed.values.get('--description'),
|
|
83
|
+
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
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ParseSpec {
|
|
95
|
+
readonly valueFlags: ReadonlySet<string>;
|
|
96
|
+
readonly booleanFlags: ReadonlySet<string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface ParsedBackendCommandArgs {
|
|
100
|
+
readonly positionals: readonly string[];
|
|
101
|
+
readonly values: ReadonlyMap<string, string>;
|
|
102
|
+
readonly booleans: ReadonlySet<string>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseBackendCommandArgs(rawArgs: readonly string[], spec: ParseSpec): ParsedBackendCommandArgs {
|
|
106
|
+
const positionals: string[] = [];
|
|
107
|
+
const values = new Map<string, string>();
|
|
108
|
+
const booleans = new Set<string>();
|
|
109
|
+
|
|
110
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
111
|
+
const arg = rawArgs[index];
|
|
112
|
+
|
|
113
|
+
if (!arg.startsWith('-')) {
|
|
114
|
+
positionals.push(arg);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (arg === '--workspace' || arg === '-w') {
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (arg === '--help' || arg === '-h') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [flag, inlineValue] = splitInlineOption(arg);
|
|
128
|
+
|
|
129
|
+
if (spec.booleanFlags.has(flag)) {
|
|
130
|
+
booleans.add(flag);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (spec.valueFlags.has(flag)) {
|
|
135
|
+
const value = inlineValue ?? rawArgs[index + 1];
|
|
136
|
+
if (!value || value.startsWith('-')) {
|
|
137
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
values.set(flag, value);
|
|
141
|
+
if (inlineValue === undefined) {
|
|
142
|
+
index += 1;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`Unknown option "${arg}".`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
positionals,
|
|
152
|
+
values,
|
|
153
|
+
booleans,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function splitInlineOption(arg: string): readonly [string, string | undefined] {
|
|
158
|
+
const equalsIndex = arg.indexOf('=');
|
|
159
|
+
if (equalsIndex < 0) {
|
|
160
|
+
return [arg, undefined];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [arg.slice(0, equalsIndex), arg.slice(equalsIndex + 1)];
|
|
164
|
+
}
|
package/src/add.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
import { runAddPage } from '@webstir-io/webstir-frontend';
|
|
6
|
+
import { runAddTest } from '@webstir-io/webstir-testing';
|
|
7
|
+
|
|
8
|
+
export interface RunAddPageOptions {
|
|
9
|
+
readonly workspaceRoot: string;
|
|
10
|
+
readonly args: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RunAddTestOptions {
|
|
14
|
+
readonly workspaceRoot: string;
|
|
15
|
+
readonly args: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AddCommandResult {
|
|
19
|
+
readonly workspaceRoot: string;
|
|
20
|
+
readonly subject: 'page' | 'test' | 'route' | 'job';
|
|
21
|
+
readonly target: string;
|
|
22
|
+
readonly changes: readonly string[];
|
|
23
|
+
readonly note?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runAddPageCommand(options: RunAddPageOptions): Promise<AddCommandResult> {
|
|
27
|
+
const pageName = options.args[0];
|
|
28
|
+
if (!pageName) {
|
|
29
|
+
throw new Error('Usage: webstir add-page <name> --workspace <path>.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const pageRoot = path.join(options.workspaceRoot, 'src', 'frontend', 'pages', pageName);
|
|
33
|
+
const trackedPaths = [
|
|
34
|
+
path.join(pageRoot, 'index.html'),
|
|
35
|
+
path.join(pageRoot, 'index.css'),
|
|
36
|
+
path.join(pageRoot, 'index.ts'),
|
|
37
|
+
path.join(pageRoot, 'tests', `${path.basename(pageName)}.test.ts`),
|
|
38
|
+
path.join(options.workspaceRoot, 'package.json'),
|
|
39
|
+
];
|
|
40
|
+
const before = await captureFileState(trackedPaths);
|
|
41
|
+
|
|
42
|
+
await runAddPage({
|
|
43
|
+
workspaceRoot: options.workspaceRoot,
|
|
44
|
+
pageName,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const changes = await collectChangedFiles(options.workspaceRoot, trackedPaths, before);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
workspaceRoot: options.workspaceRoot,
|
|
51
|
+
subject: 'page',
|
|
52
|
+
target: pageName,
|
|
53
|
+
changes,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runAddTestCommand(options: RunAddTestOptions): Promise<AddCommandResult> {
|
|
58
|
+
const nameArg = options.args[0];
|
|
59
|
+
if (!nameArg) {
|
|
60
|
+
throw new Error('Usage: webstir add-test <name-or-path> --workspace <path>.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await runAddTest({
|
|
64
|
+
workspaceRoot: options.workspaceRoot,
|
|
65
|
+
name: nameArg,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
workspaceRoot: options.workspaceRoot,
|
|
70
|
+
subject: 'test',
|
|
71
|
+
target: result.normalizedName,
|
|
72
|
+
changes: result.created ? [result.relativePath.replaceAll(path.sep, '/')] : [],
|
|
73
|
+
note: result.created ? undefined : `File already exists: ${result.relativePath.replaceAll(path.sep, '/')}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function collectChangedFiles(
|
|
78
|
+
workspaceRoot: string,
|
|
79
|
+
absolutePaths: readonly string[],
|
|
80
|
+
before: ReadonlyMap<string, string | null>
|
|
81
|
+
): Promise<string[]> {
|
|
82
|
+
const changes: string[] = [];
|
|
83
|
+
for (const absolutePath of absolutePaths) {
|
|
84
|
+
const current = await readFileIfExists(absolutePath);
|
|
85
|
+
if (current !== before.get(absolutePath)) {
|
|
86
|
+
changes.push(toWorkspaceRelative(workspaceRoot, absolutePath));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return changes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function captureFileState(absolutePaths: readonly string[]): Promise<Map<string, string | null>> {
|
|
94
|
+
const state = new Map<string, string | null>();
|
|
95
|
+
for (const absolutePath of absolutePaths) {
|
|
96
|
+
state.set(absolutePath, await readFileIfExists(absolutePath));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return state;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function readFileIfExists(absolutePath: string): Promise<string | null> {
|
|
103
|
+
if (!existsSync(absolutePath)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return await readFile(absolutePath, 'utf8');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toWorkspaceRelative(workspaceRoot: string, absolutePath: string): string {
|
|
111
|
+
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, '/');
|
|
112
|
+
}
|
package/src/api-watch.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { startBackendWatch } from '@webstir-io/webstir-backend';
|
|
3
|
+
|
|
4
|
+
import { BackendRuntimeSupervisor } from './backend-runtime.ts';
|
|
5
|
+
import { createWorkspaceRuntimeEnv } from './runtime.ts';
|
|
6
|
+
import { createStopSignal } from './stop-signal.ts';
|
|
7
|
+
import type { WorkspaceDescriptor } from './types.ts';
|
|
8
|
+
import type { WatchIo, WatchOptions } from './watch.ts';
|
|
9
|
+
|
|
10
|
+
export interface ApiWatchSession {
|
|
11
|
+
readonly origin: string;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runApiWatch(
|
|
16
|
+
workspace: WorkspaceDescriptor,
|
|
17
|
+
options: WatchOptions,
|
|
18
|
+
io: WatchIo
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const session = await startApiWatchSession(workspace, options, io);
|
|
21
|
+
|
|
22
|
+
io.stdout.write(
|
|
23
|
+
`[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${session.origin}\n`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const stopSignal = createStopSignal();
|
|
27
|
+
try {
|
|
28
|
+
await stopSignal.promise;
|
|
29
|
+
} finally {
|
|
30
|
+
stopSignal.dispose();
|
|
31
|
+
await session.stop();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function startApiWatchSession(
|
|
36
|
+
workspace: WorkspaceDescriptor,
|
|
37
|
+
options: WatchOptions,
|
|
38
|
+
io: WatchIo
|
|
39
|
+
): Promise<ApiWatchSession> {
|
|
40
|
+
const runtimeEnv = createWorkspaceRuntimeEnv(workspace.root, 'build', options.env);
|
|
41
|
+
const runtime = new BackendRuntimeSupervisor({
|
|
42
|
+
workspaceRoot: workspace.root,
|
|
43
|
+
buildRoot: path.join(workspace.root, 'build', 'backend'),
|
|
44
|
+
host: options.host ?? '127.0.0.1',
|
|
45
|
+
port: options.port,
|
|
46
|
+
env: runtimeEnv,
|
|
47
|
+
io,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await runtime.prepare();
|
|
51
|
+
|
|
52
|
+
let initialReadyLogged = false;
|
|
53
|
+
const watchHandle = await startBackendWatch({
|
|
54
|
+
workspaceRoot: workspace.root,
|
|
55
|
+
env: runtimeEnv,
|
|
56
|
+
onEvent: async (event) => {
|
|
57
|
+
if (event.type !== 'build-complete') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (event.succeeded !== true) {
|
|
62
|
+
io.stderr.write('[webstir] backend rebuild failed; keeping the current runtime process.\n');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await runtime.restart();
|
|
67
|
+
if (!initialReadyLogged) {
|
|
68
|
+
initialReadyLogged = true;
|
|
69
|
+
io.stdout.write(`[webstir] backend ready at ${runtime.getOrigin()}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
io.stdout.write(`[webstir] backend restarted at ${runtime.getOrigin()}\n`);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
origin: runtime.getOrigin(),
|
|
79
|
+
async stop() {
|
|
80
|
+
await runtime.stop();
|
|
81
|
+
await watchHandle.stop();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ModuleManifest } from '@webstir-io/module-contract';
|
|
2
|
+
import type { WorkspaceDescriptor } from './types.ts';
|
|
3
|
+
|
|
4
|
+
import { loadProvider } from './providers.ts';
|
|
5
|
+
import { createWorkspaceRuntimeEnv } from './runtime.ts';
|
|
6
|
+
import { readWorkspaceDescriptor } from './workspace.ts';
|
|
7
|
+
|
|
8
|
+
export interface RunBackendInspectOptions {
|
|
9
|
+
readonly workspaceRoot: string;
|
|
10
|
+
readonly env?: Record<string, string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BackendInspectResult {
|
|
14
|
+
readonly workspace: WorkspaceDescriptor;
|
|
15
|
+
readonly buildRoot: string;
|
|
16
|
+
readonly manifest: ModuleManifest;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runBackendInspect(options: RunBackendInspectOptions): Promise<BackendInspectResult> {
|
|
20
|
+
const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
|
|
21
|
+
if (workspace.mode !== 'api' && workspace.mode !== 'full') {
|
|
22
|
+
throw new Error(`backend-inspect only supports api and full workspaces. Received mode "${workspace.mode}".`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const provider = await loadProvider('backend');
|
|
26
|
+
const resolvedWorkspace = await provider.resolveWorkspace({
|
|
27
|
+
workspaceRoot: workspace.root,
|
|
28
|
+
config: {},
|
|
29
|
+
});
|
|
30
|
+
const result = await provider.build({
|
|
31
|
+
workspaceRoot: workspace.root,
|
|
32
|
+
env: createWorkspaceRuntimeEnv(workspace.root, 'build', options.env),
|
|
33
|
+
incremental: false,
|
|
34
|
+
});
|
|
35
|
+
const manifest = result.manifest.module;
|
|
36
|
+
if (!manifest) {
|
|
37
|
+
throw new Error('Backend manifest was not produced by the backend build.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
workspace,
|
|
42
|
+
buildRoot: resolvedWorkspace.buildRoot,
|
|
43
|
+
manifest,
|
|
44
|
+
};
|
|
45
|
+
}
|