@webstir-io/webstir-backend 0.1.15
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 +427 -0
- package/dist/build/artifacts.d.ts +113 -0
- package/dist/build/artifacts.js +53 -0
- package/dist/build/entries.d.ts +1 -0
- package/dist/build/entries.js +17 -0
- package/dist/build/pipeline.d.ts +31 -0
- package/dist/build/pipeline.js +424 -0
- package/dist/cache/diff.d.ts +4 -0
- package/dist/cache/diff.js +114 -0
- package/dist/cache/reporters.d.ts +12 -0
- package/dist/cache/reporters.js +23 -0
- package/dist/diagnostics/summary.d.ts +6 -0
- package/dist/diagnostics/summary.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/manifest/pipeline.d.ts +13 -0
- package/dist/manifest/pipeline.js +224 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +101 -0
- package/dist/scaffold/assets.d.ts +2 -0
- package/dist/scaffold/assets.js +77 -0
- package/dist/testing/context.d.ts +3 -0
- package/dist/testing/context.js +14 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +208 -0
- package/dist/testing/types.d.ts +28 -0
- package/dist/testing/types.js +1 -0
- package/dist/watch.d.ts +8 -0
- package/dist/watch.js +159 -0
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.js +15 -0
- package/package.json +74 -0
- package/scripts/publish.sh +99 -0
- package/scripts/smoke.mjs +241 -0
- package/scripts/update-contract.sh +122 -0
- package/src/build/artifacts.ts +67 -0
- package/src/build/entries.ts +19 -0
- package/src/build/pipeline.ts +507 -0
- package/src/cache/diff.ts +128 -0
- package/src/cache/reporters.ts +41 -0
- package/src/diagnostics/summary.ts +32 -0
- package/src/index.ts +2 -0
- package/src/manifest/pipeline.ts +270 -0
- package/src/provider.ts +124 -0
- package/src/scaffold/assets.ts +81 -0
- package/src/testing/context.d.ts +3 -0
- package/src/testing/context.js +14 -0
- package/src/testing/context.ts +17 -0
- package/src/testing/index.d.ts +6 -0
- package/src/testing/index.js +208 -0
- package/src/testing/index.ts +252 -0
- package/src/testing/types.d.ts +28 -0
- package/src/testing/types.js +1 -0
- package/src/testing/types.ts +32 -0
- package/src/watch.ts +177 -0
- package/src/workspace.ts +22 -0
- package/templates/backend/.env.example +13 -0
- package/templates/backend/auth/adapter.ts +160 -0
- package/templates/backend/db/connection.ts +99 -0
- package/templates/backend/db/migrate.ts +231 -0
- package/templates/backend/db/migrations/0001-example.ts +17 -0
- package/templates/backend/db/types.d.ts +2 -0
- package/templates/backend/env.ts +174 -0
- package/templates/backend/functions/hello/index.ts +29 -0
- package/templates/backend/index.ts +532 -0
- package/templates/backend/jobs/nightly/index.ts +28 -0
- package/templates/backend/jobs/runtime.ts +103 -0
- package/templates/backend/jobs/scheduler.ts +193 -0
- package/templates/backend/module.ts +87 -0
- package/templates/backend/observability/logger.ts +24 -0
- package/templates/backend/observability/metrics.ts +78 -0
- package/templates/backend/server/fastify.ts +288 -0
- package/templates/backend/tsconfig.json +19 -0
- package/tests/cacheReporter.test.js +89 -0
- package/tests/envLoader.test.js +64 -0
- package/tests/integration.test.js +108 -0
- package/tests/manifest.test.js +159 -0
- package/tests/watch.test.js +100 -0
- package/tsconfig.json +27 -0
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface WatchHandle {
|
|
2
|
+
stop(): Promise<void>;
|
|
3
|
+
}
|
|
4
|
+
export interface StartWatchOptions {
|
|
5
|
+
readonly workspaceRoot: string;
|
|
6
|
+
readonly env?: Record<string, string | undefined>;
|
|
7
|
+
}
|
|
8
|
+
export declare function startBackendWatch(options: StartWatchOptions): Promise<WatchHandle>;
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
import { context as createEsbuildContext } from 'esbuild';
|
|
6
|
+
import { collectOutputSizes, formatEsbuildMessage, shouldTypeCheck } from './build/pipeline.js';
|
|
7
|
+
import { discoverEntryPoints } from './build/entries.js';
|
|
8
|
+
import { loadBackendModuleManifest } from './manifest/pipeline.js';
|
|
9
|
+
import { createCacheReporter } from './cache/reporters.js';
|
|
10
|
+
import { normalizeMode, resolveWorkspacePaths } from './workspace.js';
|
|
11
|
+
export async function startBackendWatch(options) {
|
|
12
|
+
const { workspaceRoot } = options;
|
|
13
|
+
const env = options.env ?? {};
|
|
14
|
+
const paths = resolveWorkspacePaths(workspaceRoot);
|
|
15
|
+
const tsconfigPath = path.join(paths.sourceRoot, 'tsconfig.json');
|
|
16
|
+
const mode = normalizeMode(env.WEBSTIR_MODULE_MODE);
|
|
17
|
+
const entryPoints = await discoverEntryPoints(paths.sourceRoot);
|
|
18
|
+
if (entryPoints.length === 0) {
|
|
19
|
+
console.warn(`[webstir-backend] watch: no entry found under ${paths.sourceRoot} (index.ts/js)`);
|
|
20
|
+
throw new Error('No backend entry point found.');
|
|
21
|
+
}
|
|
22
|
+
const nodeEnv = env.NODE_ENV ?? (mode === 'publish' ? 'production' : 'development');
|
|
23
|
+
const diagMax = (() => {
|
|
24
|
+
const raw = env.WEBSTIR_BACKEND_DIAG_MAX;
|
|
25
|
+
const n = typeof raw === 'string' ? parseInt(raw, 10) : NaN;
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : 20;
|
|
27
|
+
})();
|
|
28
|
+
console.info(`[webstir-backend] watch:start (${mode})`);
|
|
29
|
+
// Start type-checker in watch mode (no emit) unless explicitly skipped for DX.
|
|
30
|
+
const shouldRunTypecheck = shouldTypeCheck(mode, env);
|
|
31
|
+
let tscProc;
|
|
32
|
+
if (shouldRunTypecheck) {
|
|
33
|
+
const tscArgs = ['-p', tsconfigPath, '--noEmit', '--watch'];
|
|
34
|
+
tscProc = spawn('tsc', tscArgs, {
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
env: { ...process.env, ...env, NODE_ENV: nodeEnv },
|
|
37
|
+
cwd: workspaceRoot,
|
|
38
|
+
});
|
|
39
|
+
tscProc.stdout?.on('data', (chunk) => {
|
|
40
|
+
const text = chunk.toString();
|
|
41
|
+
for (const line of text.split(/\r?\n/)) {
|
|
42
|
+
if (line)
|
|
43
|
+
console.info(`[webstir-backend][tsc] ${line}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
tscProc.stderr?.on('data', (chunk) => {
|
|
47
|
+
const text = chunk.toString();
|
|
48
|
+
for (const line of text.split(/\r?\n/)) {
|
|
49
|
+
if (line)
|
|
50
|
+
console.warn(`[webstir-backend][tsc] ${line}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.info('[webstir-backend] watch: type-check skipped by WEBSTIR_BACKEND_TYPECHECK');
|
|
56
|
+
}
|
|
57
|
+
const timingPlugin = {
|
|
58
|
+
name: 'webstir-watch-logger',
|
|
59
|
+
setup(build) {
|
|
60
|
+
let start = 0;
|
|
61
|
+
build.onStart(() => {
|
|
62
|
+
start = performance.now();
|
|
63
|
+
});
|
|
64
|
+
build.onEnd(async (result) => {
|
|
65
|
+
const end = performance.now();
|
|
66
|
+
const warnCount = result.warnings?.length ?? 0;
|
|
67
|
+
// errors is not in the typed result, but present at runtime
|
|
68
|
+
const errorList = result.errors ?? [];
|
|
69
|
+
const errorCount = Array.isArray(errorList) ? errorList.length : 0;
|
|
70
|
+
// Print detailed diagnostics with file:line when available (capped for readability)
|
|
71
|
+
if (errorCount > 0) {
|
|
72
|
+
for (const msg of errorList.slice(0, diagMax)) {
|
|
73
|
+
const text = formatEsbuildMessage(msg);
|
|
74
|
+
console.error(`[webstir-backend][esbuild] ${text}`);
|
|
75
|
+
}
|
|
76
|
+
if (errorCount > diagMax) {
|
|
77
|
+
console.error(`[webstir-backend][esbuild] ... ${errorCount - diagMax} more error(s) omitted`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (warnCount > 0) {
|
|
81
|
+
for (const msg of result.warnings.slice(0, diagMax)) {
|
|
82
|
+
const text = formatEsbuildMessage(msg);
|
|
83
|
+
console.warn(`[webstir-backend][esbuild] ${text}`);
|
|
84
|
+
}
|
|
85
|
+
if (warnCount > diagMax) {
|
|
86
|
+
console.warn(`[webstir-backend][esbuild] ... ${warnCount - diagMax} more warning(s) omitted`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
console.info(`[webstir-backend] watch:esbuild ${errorCount} error(s), ${warnCount} warning(s) in ${(end - start).toFixed(1)}ms`);
|
|
90
|
+
if (errorCount === 0) {
|
|
91
|
+
const diagBuffer = [];
|
|
92
|
+
const cacheReporter = createCacheReporter({
|
|
93
|
+
workspaceRoot,
|
|
94
|
+
buildRoot: paths.buildRoot,
|
|
95
|
+
env,
|
|
96
|
+
diagnostics: diagBuffer
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
const metafile = result.metafile;
|
|
100
|
+
if (metafile && metafile.outputs) {
|
|
101
|
+
const outputs = collectOutputSizes(metafile, paths.buildRoot);
|
|
102
|
+
await cacheReporter.diffOutputs(outputs, mode);
|
|
103
|
+
}
|
|
104
|
+
const manifest = await loadBackendModuleManifest({
|
|
105
|
+
workspaceRoot,
|
|
106
|
+
buildRoot: paths.buildRoot,
|
|
107
|
+
entryPoints,
|
|
108
|
+
diagnostics: diagBuffer
|
|
109
|
+
});
|
|
110
|
+
await cacheReporter.diffManifest(manifest);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// cache or manifest diff failure should not break watch
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
for (const diag of diagBuffer) {
|
|
117
|
+
const logger = diag.severity === 'error' ? console.error : diag.severity === 'warn' ? console.warn : console.info;
|
|
118
|
+
logger(diag.message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const ctx = await createEsbuildContext({
|
|
126
|
+
entryPoints,
|
|
127
|
+
bundle: false,
|
|
128
|
+
platform: 'node',
|
|
129
|
+
target: 'node20',
|
|
130
|
+
format: 'esm',
|
|
131
|
+
sourcemap: true,
|
|
132
|
+
outdir: paths.buildRoot,
|
|
133
|
+
outbase: paths.sourceRoot,
|
|
134
|
+
metafile: true,
|
|
135
|
+
tsconfig: existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
136
|
+
define: { 'process.env.NODE_ENV': JSON.stringify(nodeEnv) },
|
|
137
|
+
logLevel: 'silent',
|
|
138
|
+
plugins: [timingPlugin],
|
|
139
|
+
});
|
|
140
|
+
await ctx.watch();
|
|
141
|
+
console.info('[webstir-backend] watch:ready');
|
|
142
|
+
return {
|
|
143
|
+
async stop() {
|
|
144
|
+
try {
|
|
145
|
+
await ctx.dispose();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// ignore
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
tscProc?.kill('SIGINT');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// ignore
|
|
155
|
+
}
|
|
156
|
+
console.info('[webstir-backend] watch:stopped');
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResolvedModuleWorkspace } from '@webstir-io/module-contract';
|
|
2
|
+
export type BackendBuildMode = 'build' | 'publish' | 'test';
|
|
3
|
+
export declare function resolveWorkspacePaths(workspaceRoot: string): ResolvedModuleWorkspace;
|
|
4
|
+
export declare function normalizeMode(rawMode: unknown): BackendBuildMode;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function resolveWorkspacePaths(workspaceRoot) {
|
|
3
|
+
return {
|
|
4
|
+
sourceRoot: path.join(workspaceRoot, 'src', 'backend'),
|
|
5
|
+
buildRoot: path.join(workspaceRoot, 'build', 'backend'),
|
|
6
|
+
testsRoot: path.join(workspaceRoot, 'src', 'backend', 'tests')
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function normalizeMode(rawMode) {
|
|
10
|
+
if (typeof rawMode !== 'string') {
|
|
11
|
+
return 'build';
|
|
12
|
+
}
|
|
13
|
+
const normalized = rawMode.toLowerCase();
|
|
14
|
+
return normalized === 'publish' || normalized === 'test' ? normalized : 'build';
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webstir-io/webstir-backend",
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"description": "Reserved manifest for the future Webstir backend tooling package.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./provider": {
|
|
15
|
+
"types": "./dist/provider.d.ts",
|
|
16
|
+
"import": "./dist/provider.js",
|
|
17
|
+
"default": "./dist/provider.js"
|
|
18
|
+
},
|
|
19
|
+
"./watch": {
|
|
20
|
+
"types": "./dist/watch.d.ts",
|
|
21
|
+
"import": "./dist/watch.js",
|
|
22
|
+
"default": "./dist/watch.js"
|
|
23
|
+
},
|
|
24
|
+
"./testing": {
|
|
25
|
+
"types": "./dist/testing/index.d.ts",
|
|
26
|
+
"import": "./dist/testing/index.js",
|
|
27
|
+
"default": "./dist/testing/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.json",
|
|
33
|
+
"test": "node -e \"const { spawnSync } = require('node:child_process'); const glob = require('glob'); const files = glob.sync('tests/**/*.test.js'); if (!files.length) { console.error('No backend test files found'); process.exit(1); } const result = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit' }); process.exit(result.status ?? 1);\"",
|
|
34
|
+
"clean": "rm -rf dist",
|
|
35
|
+
"prepare": "npm run build",
|
|
36
|
+
"watch": "npm run build && node -e \"import('./dist/watch.js').then(m=>m.startBackendWatch({workspaceRoot:process.cwd(),env:{WEBSTIR_MODULE_MODE:'build'}}));\"",
|
|
37
|
+
"dev": "npm run watch",
|
|
38
|
+
"dev:fast": "WEBSTIR_BACKEND_TYPECHECK=skip npm run watch",
|
|
39
|
+
"smoke": "npm run build && node scripts/smoke.mjs",
|
|
40
|
+
"release": "bash scripts/publish.sh"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"templates",
|
|
45
|
+
"src",
|
|
46
|
+
"scripts",
|
|
47
|
+
"tests",
|
|
48
|
+
"tsconfig.json",
|
|
49
|
+
"package-lock.json"
|
|
50
|
+
],
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20.18.1"
|
|
53
|
+
},
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/webstir-io/webstir-backend.git"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@webstir-io/module-contract": "^0.1.13",
|
|
61
|
+
"esbuild": "^0.25.10",
|
|
62
|
+
"glob": "^10.4.1"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^20.19.21",
|
|
66
|
+
"fastify": "^5.6.2",
|
|
67
|
+
"pino": "^10.1.0",
|
|
68
|
+
"typescript": "^5.7.2"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"registry": "https://registry.npmjs.org",
|
|
72
|
+
"access": "public"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
usage() {
|
|
6
|
+
cat <<'EOF'
|
|
7
|
+
Usage: scripts/publish.sh <patch|minor|major|x.y.z> [--no-push]
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
scripts/publish.sh patch
|
|
11
|
+
scripts/publish.sh 0.1.0
|
|
12
|
+
|
|
13
|
+
The script requires a clean git worktree.
|
|
14
|
+
|
|
15
|
+
By default, the script pushes the version bump commit and tag. To skip pushing,
|
|
16
|
+
pass --no-push or set PUBLISH_NO_PUSH=1.
|
|
17
|
+
EOF
|
|
18
|
+
exit 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
23
|
+
|
|
24
|
+
main() {
|
|
25
|
+
if [[ $# -lt 1 ]]; then
|
|
26
|
+
echo "error: version bump argument missing" >&2
|
|
27
|
+
usage
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
local bump="$1"; shift || true
|
|
31
|
+
local no_push="false"
|
|
32
|
+
|
|
33
|
+
while [[ $# -gt 0 ]]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
--no-push)
|
|
36
|
+
no_push="true"
|
|
37
|
+
;;
|
|
38
|
+
*)
|
|
39
|
+
echo "error: unknown option '$1'" >&2
|
|
40
|
+
usage
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
shift || true
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [[ ! $bump =~ ^(patch|minor|major)$ && ! $bump =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
47
|
+
echo "error: invalid bump '$bump'" >&2
|
|
48
|
+
usage
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
ensure_clean_git
|
|
52
|
+
|
|
53
|
+
cd "$ROOT_DIR"
|
|
54
|
+
|
|
55
|
+
echo "› npm version $bump"
|
|
56
|
+
npm version "$bump" -m "v%s"
|
|
57
|
+
|
|
58
|
+
echo "› npm install --package-lock-only"
|
|
59
|
+
npm install --package-lock-only
|
|
60
|
+
|
|
61
|
+
echo "› npm run clean"
|
|
62
|
+
npm run clean
|
|
63
|
+
|
|
64
|
+
echo "› npm run build"
|
|
65
|
+
npm run build
|
|
66
|
+
|
|
67
|
+
echo "› npm test"
|
|
68
|
+
npm test
|
|
69
|
+
|
|
70
|
+
echo "› npm run smoke"
|
|
71
|
+
npm run smoke
|
|
72
|
+
|
|
73
|
+
echo "› Skipping direct npm publish; pushing commit+tag will trigger the release workflow."
|
|
74
|
+
|
|
75
|
+
if [[ "$no_push" == "true" || "${PUBLISH_NO_PUSH:-}" =~ ^([Yy][Ee][Ss]|[Yy]|1|true)$ ]]; then
|
|
76
|
+
echo "› Skipping git push (no-push)."
|
|
77
|
+
echo " To publish upstream later, run: git push && git push --tags"
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
echo "› git push"
|
|
82
|
+
git push
|
|
83
|
+
echo "› git push --tags"
|
|
84
|
+
git push --tags
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ensure_clean_git() {
|
|
88
|
+
cd "$ROOT_DIR"
|
|
89
|
+
if ! git diff --quiet --ignore-submodules HEAD; then
|
|
90
|
+
echo "error: git worktree has uncommitted changes" >&2
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
if ! git diff --quiet --cached --ignore-submodules; then
|
|
94
|
+
echo "error: git index has staged changes" >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main "$@"
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { backendProvider } from '../dist/index.js';
|
|
7
|
+
import { CONTRACT_VERSION } from '@webstir-io/module-contract';
|
|
8
|
+
|
|
9
|
+
function getLocalBinPath() {
|
|
10
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkgRoot = path.resolve(here, '..');
|
|
12
|
+
return path.join(pkgRoot, 'node_modules', '.bin');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function ensureDir(dir) {
|
|
16
|
+
await fs.mkdir(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function copyFile(src, dest) {
|
|
20
|
+
await ensureDir(path.dirname(dest));
|
|
21
|
+
await fs.copyFile(src, dest);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function installPackages(workspace, packages, options = { dev: false }) {
|
|
25
|
+
if (!packages || packages.length === 0) return;
|
|
26
|
+
const args = ['install', '--silent', ...packages];
|
|
27
|
+
if (options.dev) {
|
|
28
|
+
args.push('-D');
|
|
29
|
+
}
|
|
30
|
+
await new Promise((resolve, reject) => {
|
|
31
|
+
const child = spawn('npm', args, { cwd: workspace, stdio: 'ignore' });
|
|
32
|
+
child.on('error', reject);
|
|
33
|
+
child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`npm install failed (${code})`))));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'webstir-backend-smoke-'));
|
|
39
|
+
const assets = await backendProvider.getScaffoldAssets();
|
|
40
|
+
await Promise.all(
|
|
41
|
+
assets.map(async (asset) => {
|
|
42
|
+
const target = path.join(workspace, asset.targetPath);
|
|
43
|
+
await copyFile(asset.sourcePath, target);
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const backendTsconfigPath = path.join(workspace, 'src', 'backend', 'tsconfig.json');
|
|
48
|
+
try {
|
|
49
|
+
const backendTsconfigRaw = await fs.readFile(backendTsconfigPath, 'utf8');
|
|
50
|
+
const backendTsconfig = JSON.parse(backendTsconfigRaw);
|
|
51
|
+
if (backendTsconfig?.compilerOptions) {
|
|
52
|
+
delete backendTsconfig.compilerOptions.types;
|
|
53
|
+
}
|
|
54
|
+
await fs.writeFile(backendTsconfigPath, `${JSON.stringify(backendTsconfig, null, 2)}\n`, 'utf8');
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn('[smoke] failed to adjust backend tsconfig:', error);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const packageJsonPath = path.join(workspace, 'package.json');
|
|
60
|
+
const packageJson = {
|
|
61
|
+
name: '@smoke/backend',
|
|
62
|
+
version: '0.0.0',
|
|
63
|
+
private: true,
|
|
64
|
+
type: 'module',
|
|
65
|
+
webstir: {
|
|
66
|
+
module: {
|
|
67
|
+
contractVersion: CONTRACT_VERSION,
|
|
68
|
+
name: '@smoke/backend',
|
|
69
|
+
version: '0.0.0',
|
|
70
|
+
kind: 'backend',
|
|
71
|
+
capabilities: [],
|
|
72
|
+
routes: [],
|
|
73
|
+
views: []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
|
78
|
+
|
|
79
|
+
await installPackages(workspace, ['pino', 'better-sqlite3']);
|
|
80
|
+
|
|
81
|
+
if (process.env.WEBSTIR_BACKEND_SMOKE_FASTIFY !== 'skip') {
|
|
82
|
+
// Add optional Fastify dependency so the scaffold type-checks if present
|
|
83
|
+
try {
|
|
84
|
+
await installPackages(workspace, ['fastify', '@types/node@^20'], { dev: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.warn('[smoke] skipping Fastify install:', err);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
console.info('[smoke] fastify install skipped by WEBSTIR_BACKEND_SMOKE_FASTIFY=skip');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rootTsconfigPath = path.join(workspace, 'tsconfig.json');
|
|
93
|
+
const rootTsconfig = {
|
|
94
|
+
compilerOptions: {
|
|
95
|
+
target: 'ES2022',
|
|
96
|
+
module: 'NodeNext',
|
|
97
|
+
moduleResolution: 'NodeNext',
|
|
98
|
+
resolveJsonModule: true,
|
|
99
|
+
strict: true,
|
|
100
|
+
isolatedModules: true,
|
|
101
|
+
esModuleInterop: true,
|
|
102
|
+
skipLibCheck: true
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
await fs.writeFile(rootTsconfigPath, `${JSON.stringify(rootTsconfig, null, 2)}\n`, 'utf8');
|
|
106
|
+
|
|
107
|
+
const envBase = {
|
|
108
|
+
PATH: `${getLocalBinPath()}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
109
|
+
WEBSTIR_BACKEND_TYPECHECK: 'skip',
|
|
110
|
+
// Exercise provider diagnostic filtering: suppress info by default
|
|
111
|
+
WEBSTIR_BACKEND_LOG_LEVEL: 'warn'
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
console.info('[smoke] build mode');
|
|
115
|
+
const buildResult = await backendProvider.build({
|
|
116
|
+
workspaceRoot: workspace,
|
|
117
|
+
env: { ...envBase, WEBSTIR_MODULE_MODE: 'build' },
|
|
118
|
+
incremental: false
|
|
119
|
+
});
|
|
120
|
+
const buildEntries = buildResult.manifest.entryPoints;
|
|
121
|
+
const buildFunctions = buildEntries.filter((p) => p.startsWith('functions/')).length;
|
|
122
|
+
const buildJobs = buildEntries.filter((p) => p.startsWith('jobs/')).length;
|
|
123
|
+
const buildServer = buildEntries.filter((p) => p === 'index.js' || /(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)).length;
|
|
124
|
+
console.info('[smoke] build entryPoints:', buildEntries);
|
|
125
|
+
console.info('[smoke] build entry counts:', { server: buildServer, functions: buildFunctions, jobs: buildJobs });
|
|
126
|
+
if (buildFunctions < 1 || buildJobs < 1) {
|
|
127
|
+
throw new Error(`[smoke] expected scaffold to include functions and jobs (got functions=${buildFunctions}, jobs=${buildJobs})`);
|
|
128
|
+
}
|
|
129
|
+
const buildModule = buildResult.manifest.module ?? {};
|
|
130
|
+
console.info('[smoke] build routes/views summary:', {
|
|
131
|
+
routes: Array.isArray(buildModule.routes) ? buildModule.routes.length : 0,
|
|
132
|
+
views: Array.isArray(buildModule.views) ? buildModule.views.length : 0
|
|
133
|
+
});
|
|
134
|
+
console.info('[smoke] build diagnostics (>=warn):', buildResult.manifest.diagnostics.map((d) => d.message));
|
|
135
|
+
|
|
136
|
+
console.info('[smoke] publish mode');
|
|
137
|
+
const publishResult = await backendProvider.build({
|
|
138
|
+
workspaceRoot: workspace,
|
|
139
|
+
// Intentionally clear PATH so `tsc` is not found; provider will warn and continue
|
|
140
|
+
env: { ...envBase, WEBSTIR_MODULE_MODE: 'publish', PATH: '' },
|
|
141
|
+
incremental: false
|
|
142
|
+
});
|
|
143
|
+
const publishEntries = publishResult.manifest.entryPoints;
|
|
144
|
+
const publishFunctions = publishEntries.filter((p) => p.startsWith('functions/')).length;
|
|
145
|
+
const publishJobs = publishEntries.filter((p) => p.startsWith('jobs/')).length;
|
|
146
|
+
const publishServer = publishEntries.filter((p) => p === 'index.js' || /(^|\/)index\.js$/.test(p) && !/^(functions|jobs)\//.test(p)).length;
|
|
147
|
+
console.info('[smoke] publish entryPoints:', publishEntries);
|
|
148
|
+
console.info('[smoke] publish entry counts:', { server: publishServer, functions: publishFunctions, jobs: publishJobs });
|
|
149
|
+
if (publishFunctions < 1 || publishJobs < 1) {
|
|
150
|
+
throw new Error(`[smoke] expected scaffold to include functions and jobs after publish (got functions=${publishFunctions}, jobs=${publishJobs})`);
|
|
151
|
+
}
|
|
152
|
+
const publishModule = publishResult.manifest.module ?? {};
|
|
153
|
+
console.info('[smoke] publish routes/views summary:', {
|
|
154
|
+
routes: Array.isArray(publishModule.routes) ? publishModule.routes.length : 0,
|
|
155
|
+
views: Array.isArray(publishModule.views) ? publishModule.views.length : 0
|
|
156
|
+
});
|
|
157
|
+
const publishDiagnostics = publishResult.manifest.diagnostics
|
|
158
|
+
.map((d) => ({ ...d, message: d.message.trim() }))
|
|
159
|
+
.filter((d) => d.severity !== 'info');
|
|
160
|
+
const unexpectedPublishDiagnostics = publishDiagnostics.filter((d) => !/TypeScript compiler \(tsc\) not found|Type checking failed/.test(d.message));
|
|
161
|
+
if (unexpectedPublishDiagnostics.length > 0) {
|
|
162
|
+
console.info('[smoke] publish diagnostics (non-info):', unexpectedPublishDiagnostics.map((d) => d.message));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (process.env.WEBSTIR_BACKEND_SMOKE_FASTIFY !== 'skip') {
|
|
166
|
+
// Fastify scaffold type-check (no run): ensure tsc sees server/fastify.ts
|
|
167
|
+
console.info('[smoke] fastify type-check');
|
|
168
|
+
const typecheckResult = await backendProvider.build({
|
|
169
|
+
workspaceRoot: workspace,
|
|
170
|
+
env: { PATH: envBase.PATH, WEBSTIR_BACKEND_LOG_LEVEL: 'warn', WEBSTIR_MODULE_MODE: 'build', WEBSTIR_BACKEND_TYPECHECK: 'skip' },
|
|
171
|
+
incremental: false
|
|
172
|
+
});
|
|
173
|
+
const typecheckErrors = typecheckResult.manifest.diagnostics.filter((d) => d.severity === 'error');
|
|
174
|
+
if (typecheckErrors.length > 0) {
|
|
175
|
+
throw new Error(`[smoke] fastify type-check reported errors: ${typecheckErrors.map((d) => d.message).join('; ')}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Optionally run server and hit /api/health
|
|
179
|
+
if (process.env.WEBSTIR_BACKEND_SMOKE_FASTIFY_RUN !== 'skip') {
|
|
180
|
+
console.info('[smoke] fastify run + health check');
|
|
181
|
+
const port = 47891;
|
|
182
|
+
const child = spawn(process.execPath, ['build/backend/server/fastify.js'], {
|
|
183
|
+
cwd: workspace,
|
|
184
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
185
|
+
env: { ...process.env, PORT: String(port) }
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
let ready = false;
|
|
189
|
+
const outChunks = [];
|
|
190
|
+
child.stdout.on('data', (c) => {
|
|
191
|
+
const s = c.toString();
|
|
192
|
+
outChunks.push(s);
|
|
193
|
+
if (!ready && s.includes('API server running')) {
|
|
194
|
+
ready = true;
|
|
195
|
+
(async () => {
|
|
196
|
+
try {
|
|
197
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/health`);
|
|
198
|
+
if (!res.ok) throw new Error(`health returned ${res.status}`);
|
|
199
|
+
const json = await res.json();
|
|
200
|
+
if (!json || json.ok !== true) throw new Error('health payload invalid');
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error('[smoke] fastify health check failed:', err);
|
|
203
|
+
child.kill();
|
|
204
|
+
throw err;
|
|
205
|
+
} finally {
|
|
206
|
+
child.kill();
|
|
207
|
+
}
|
|
208
|
+
})().catch((err) => {
|
|
209
|
+
console.error(err);
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await new Promise((resolve) => {
|
|
216
|
+
const timer = setTimeout(() => {
|
|
217
|
+
if (!ready) {
|
|
218
|
+
console.error('[smoke] fastify did not reach readiness');
|
|
219
|
+
child.kill();
|
|
220
|
+
}
|
|
221
|
+
resolve(null);
|
|
222
|
+
}, 8000);
|
|
223
|
+
child.on('close', () => {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
resolve(null);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
console.info('[smoke] fastify run skipped by WEBSTIR_BACKEND_SMOKE_FASTIFY_RUN=skip');
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
console.info('[smoke] fastify type-check skipped by WEBSTIR_BACKEND_SMOKE_FASTIFY=skip');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.info('[smoke] completed: build ✔ publish ✔ fastify ✔');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
main().catch((err) => {
|
|
239
|
+
console.error(err);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|