@webstir-io/webstir-backend 0.1.15 → 0.1.16
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 +106 -79
- package/dist/add.d.ts +59 -0
- package/dist/add.js +626 -0
- package/dist/build/artifacts.d.ts +115 -1
- package/dist/build/artifacts.js +4 -4
- package/dist/build/entries.js +1 -1
- package/dist/build/pipeline.d.ts +33 -1
- package/dist/build/pipeline.js +307 -65
- package/dist/cache/diff.js +9 -8
- package/dist/cache/reporters.js +1 -1
- package/dist/deploy-cli.d.ts +2 -0
- package/dist/deploy-cli.js +86 -0
- package/dist/diagnostics/summary.js +2 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/manifest/pipeline.js +103 -32
- package/dist/provider.js +35 -17
- package/dist/runtime/bun.d.ts +51 -0
- package/dist/runtime/bun.js +499 -0
- package/dist/runtime/core.d.ts +141 -0
- package/dist/runtime/core.js +316 -0
- package/dist/runtime/deploy-backend.d.ts +20 -0
- package/dist/runtime/deploy-backend.js +175 -0
- package/dist/runtime/deploy-shared.d.ts +43 -0
- package/dist/runtime/deploy-shared.js +75 -0
- package/dist/runtime/deploy-static.d.ts +2 -0
- package/dist/runtime/deploy-static.js +161 -0
- package/dist/runtime/deploy.d.ts +3 -0
- package/dist/runtime/deploy.js +91 -0
- package/dist/runtime/forms.d.ts +73 -0
- package/dist/runtime/forms.js +236 -0
- package/dist/runtime/request-hooks.d.ts +47 -0
- package/dist/runtime/request-hooks.js +102 -0
- package/dist/runtime/session-metadata.d.ts +13 -0
- package/dist/runtime/session-metadata.js +98 -0
- package/dist/runtime/session-runtime.d.ts +28 -0
- package/dist/runtime/session-runtime.js +180 -0
- package/dist/runtime/session.d.ts +83 -0
- package/dist/runtime/session.js +396 -0
- package/dist/runtime/views.d.ts +74 -0
- package/dist/runtime/views.js +221 -0
- package/dist/scaffold/assets.js +25 -21
- package/dist/testing/context.js +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +100 -56
- package/dist/utils/bun.d.ts +2 -0
- package/dist/utils/bun.js +13 -0
- package/dist/watch.d.ts +13 -1
- package/dist/watch.js +345 -97
- package/dist/workspace.d.ts +8 -0
- package/dist/workspace.js +44 -3
- package/package.json +49 -14
- package/scripts/publish.sh +2 -92
- package/scripts/smoke.mjs +282 -107
- package/scripts/update-contract.sh +12 -10
- package/src/add.ts +964 -0
- package/src/build/artifacts.ts +49 -46
- package/src/build/entries.ts +12 -12
- package/src/build/pipeline.ts +779 -403
- package/src/cache/diff.ts +111 -105
- package/src/cache/reporters.ts +26 -26
- package/src/deploy-cli.ts +111 -0
- package/src/diagnostics/summary.ts +28 -22
- package/src/index.ts +11 -0
- package/src/manifest/pipeline.ts +328 -215
- package/src/provider.ts +115 -98
- package/src/runtime/bun.ts +793 -0
- package/src/runtime/core.ts +598 -0
- package/src/runtime/deploy-backend.ts +239 -0
- package/src/runtime/deploy-shared.ts +136 -0
- package/src/runtime/deploy-static.ts +191 -0
- package/src/runtime/deploy.ts +143 -0
- package/src/runtime/forms.ts +364 -0
- package/src/runtime/request-hooks.ts +165 -0
- package/src/runtime/session-metadata.ts +135 -0
- package/src/runtime/session-runtime.ts +267 -0
- package/src/runtime/session.ts +642 -0
- package/src/runtime/views.ts +385 -0
- package/src/scaffold/assets.ts +77 -73
- package/src/testing/context.js +8 -9
- package/src/testing/context.ts +9 -9
- package/src/testing/index.d.ts +14 -3
- package/src/testing/index.js +254 -175
- package/src/testing/index.ts +298 -195
- package/src/testing/types.d.ts +18 -19
- package/src/testing/types.ts +18 -18
- package/src/utils/bun.ts +26 -0
- package/src/watch.ts +503 -99
- package/src/workspace.ts +59 -3
- package/templates/backend/.env.example +15 -0
- package/templates/backend/auth/adapter.ts +335 -36
- package/templates/backend/db/connection.ts +190 -65
- package/templates/backend/db/migrate.ts +149 -43
- package/templates/backend/db/types.d.ts +1 -1
- package/templates/backend/env.ts +132 -20
- package/templates/backend/functions/hello/index.ts +1 -2
- package/templates/backend/index.ts +15 -508
- package/templates/backend/jobs/nightly/index.ts +1 -1
- package/templates/backend/jobs/runtime.ts +24 -11
- package/templates/backend/jobs/scheduler.ts +208 -46
- package/templates/backend/module.ts +227 -13
- package/templates/backend/observability/logger.ts +2 -12
- package/templates/backend/observability/metrics.ts +8 -5
- package/templates/backend/session/sqlite.ts +152 -0
- package/templates/backend/session/store.ts +45 -0
- package/templates/backend/tsconfig.json +1 -1
- package/tests/add.test.js +327 -0
- package/tests/authAdapter.test.js +315 -0
- package/tests/bundlerParity.test.js +217 -0
- package/tests/cacheReporter.test.js +10 -10
- package/tests/dbConnection.test.js +209 -0
- package/tests/deploy.test.js +357 -0
- package/tests/envLoader.test.js +271 -17
- package/tests/integration.test.js +2432 -3
- package/tests/jobsScheduler.test.js +253 -0
- package/tests/manifest.test.js +287 -12
- package/tests/migrationRunner.test.js +249 -0
- package/tests/sessionScaffoldStore.test.js +752 -0
- package/tests/sessionStore.test.js +490 -0
- package/tests/testing.test.js +252 -0
- package/tests/watch.test.js +192 -32
- package/tsconfig.json +3 -10
- package/templates/backend/server/fastify.ts +0 -288
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { access, stat } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { resolveWorkspaceRoot } from '../workspace.js';
|
|
5
|
+
import { readTextFile } from '../utils/bun.js';
|
|
6
|
+
|
|
7
|
+
export interface EnvAccessorLike {
|
|
8
|
+
get(name: string): string | undefined;
|
|
9
|
+
require(name: string): string;
|
|
10
|
+
entries(): Record<string, string | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LoggerLike {
|
|
14
|
+
readonly level?: string;
|
|
15
|
+
log?(level: string, message: string, metadata?: Record<string, unknown>): void;
|
|
16
|
+
debug?(message: string, metadata?: Record<string, unknown>): void;
|
|
17
|
+
info?(message: string, metadata?: Record<string, unknown>): void;
|
|
18
|
+
warn?(message: string, metadata?: Record<string, unknown>): void;
|
|
19
|
+
error?(message: string, metadata?: Record<string, unknown>): void;
|
|
20
|
+
with?(bindings: Record<string, unknown>): LoggerLike;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ViewDefinitionLike {
|
|
24
|
+
name?: string;
|
|
25
|
+
path?: string;
|
|
26
|
+
renderMode?: 'ssg' | 'ssr' | 'spa';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RequestTimeDocumentCacheStatus = 'miss' | 'hit' | 'stale';
|
|
30
|
+
|
|
31
|
+
export interface RequestTimeDocumentCacheMetadata {
|
|
32
|
+
readonly status: RequestTimeDocumentCacheStatus;
|
|
33
|
+
readonly documentPath: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RenderedRequestTimeView {
|
|
37
|
+
readonly html: string;
|
|
38
|
+
readonly documentCache: RequestTimeDocumentCacheMetadata;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SSRContextLike {
|
|
42
|
+
readonly url: URL;
|
|
43
|
+
readonly params: Record<string, string>;
|
|
44
|
+
readonly cookies: Record<string, string>;
|
|
45
|
+
readonly headers: Record<string, string>;
|
|
46
|
+
readonly auth: unknown;
|
|
47
|
+
readonly session: Record<string, unknown> | null;
|
|
48
|
+
readonly env: EnvAccessorLike;
|
|
49
|
+
readonly logger: LoggerLike;
|
|
50
|
+
readonly requestId?: string;
|
|
51
|
+
readonly now: () => Date;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ModuleViewLike {
|
|
55
|
+
readonly definition?: ViewDefinitionLike;
|
|
56
|
+
readonly load?: (context: SSRContextLike) => Promise<unknown> | unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CompiledView {
|
|
60
|
+
readonly name: string;
|
|
61
|
+
readonly pathPattern: string;
|
|
62
|
+
readonly definition?: ViewDefinitionLike;
|
|
63
|
+
readonly load?: ModuleViewLike['load'];
|
|
64
|
+
readonly match: (pathname: string) => {
|
|
65
|
+
matched: boolean;
|
|
66
|
+
params: Record<string, string>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function compileViews(views: readonly ModuleViewLike[]): CompiledView[] {
|
|
71
|
+
const compiled: CompiledView[] = [];
|
|
72
|
+
for (const view of views) {
|
|
73
|
+
const pathPattern = normalizePath(view.definition?.path ?? '/');
|
|
74
|
+
compiled.push({
|
|
75
|
+
name: view.definition?.name ?? pathPattern,
|
|
76
|
+
pathPattern,
|
|
77
|
+
definition: view.definition,
|
|
78
|
+
load: view.load,
|
|
79
|
+
match: createPathMatcher(pathPattern),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return compiled;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function matchView(
|
|
86
|
+
views: readonly CompiledView[],
|
|
87
|
+
pathname: string,
|
|
88
|
+
): { view: CompiledView; params: Record<string, string> } | undefined {
|
|
89
|
+
for (const view of views) {
|
|
90
|
+
const matched = view.match(pathname);
|
|
91
|
+
if (matched.matched) {
|
|
92
|
+
return {
|
|
93
|
+
view,
|
|
94
|
+
params: matched.params,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function renderRequestTimeView(options: {
|
|
102
|
+
workspaceRoot?: string;
|
|
103
|
+
url: URL;
|
|
104
|
+
view: CompiledView;
|
|
105
|
+
params: Record<string, string>;
|
|
106
|
+
cookies: Record<string, string>;
|
|
107
|
+
headers: Record<string, string>;
|
|
108
|
+
auth: unknown;
|
|
109
|
+
session: Record<string, unknown> | null;
|
|
110
|
+
env: EnvAccessorLike;
|
|
111
|
+
logger: LoggerLike;
|
|
112
|
+
requestId?: string;
|
|
113
|
+
now?: () => Date;
|
|
114
|
+
}): Promise<RenderedRequestTimeView> {
|
|
115
|
+
const {
|
|
116
|
+
workspaceRoot,
|
|
117
|
+
url,
|
|
118
|
+
view,
|
|
119
|
+
params,
|
|
120
|
+
cookies,
|
|
121
|
+
headers,
|
|
122
|
+
auth,
|
|
123
|
+
session,
|
|
124
|
+
env,
|
|
125
|
+
logger,
|
|
126
|
+
requestId,
|
|
127
|
+
} = options;
|
|
128
|
+
const now = options.now ?? (() => new Date());
|
|
129
|
+
const document = await loadFrontendDocument(resolveWorkspaceRoot(workspaceRoot), url.pathname);
|
|
130
|
+
|
|
131
|
+
const viewData = view.load
|
|
132
|
+
? await view.load({
|
|
133
|
+
url,
|
|
134
|
+
params,
|
|
135
|
+
cookies,
|
|
136
|
+
headers,
|
|
137
|
+
auth,
|
|
138
|
+
session,
|
|
139
|
+
env,
|
|
140
|
+
logger,
|
|
141
|
+
requestId,
|
|
142
|
+
now,
|
|
143
|
+
})
|
|
144
|
+
: null;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
html: injectViewState(document.html, {
|
|
148
|
+
name: view.name,
|
|
149
|
+
templatePath: view.pathPattern,
|
|
150
|
+
pathname: normalizePath(url.pathname),
|
|
151
|
+
params,
|
|
152
|
+
data: viewData ?? null,
|
|
153
|
+
requestId,
|
|
154
|
+
}),
|
|
155
|
+
documentCache: {
|
|
156
|
+
status: document.cacheStatus,
|
|
157
|
+
documentPath: document.path,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function toHeaderRecord(
|
|
163
|
+
headers: Record<string, string | string[] | undefined>,
|
|
164
|
+
): Record<string, string> {
|
|
165
|
+
const normalized: Record<string, string> = {};
|
|
166
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
normalized[key] = value;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
normalized[key] = value.join(', ');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return normalized;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function createPathMatcher(pattern: string) {
|
|
179
|
+
const normalized = normalizePath(pattern);
|
|
180
|
+
const paramRegex = /:([A-Za-z0-9_]+)/g;
|
|
181
|
+
const regex = new RegExp(
|
|
182
|
+
'^' +
|
|
183
|
+
normalized
|
|
184
|
+
.replace(/\//g, '\\/')
|
|
185
|
+
.replace(paramRegex, (_segment, name) => `(?<${name}>[^/]+)`) +
|
|
186
|
+
'$',
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (pathname: string) => {
|
|
190
|
+
const pathToTest = normalizePath(pathname);
|
|
191
|
+
const match = regex.exec(pathToTest);
|
|
192
|
+
if (!match) {
|
|
193
|
+
return { matched: false, params: {} };
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
matched: true,
|
|
197
|
+
params: (match.groups ?? {}) as Record<string, string>,
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizePath(value: string | undefined): string {
|
|
203
|
+
if (!value || value === '/') {
|
|
204
|
+
return '/';
|
|
205
|
+
}
|
|
206
|
+
const trimmed = value.endsWith('/') ? value.slice(0, -1) : value;
|
|
207
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function firstPathSegment(pathname: string): string | undefined {
|
|
211
|
+
const normalized = normalizePath(pathname);
|
|
212
|
+
if (normalized === '/') {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
216
|
+
return parts[0];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const documentTemplateCache = new Map<
|
|
220
|
+
string,
|
|
221
|
+
{
|
|
222
|
+
html: string;
|
|
223
|
+
ctimeMs: number;
|
|
224
|
+
mtimeMs: number;
|
|
225
|
+
size: number;
|
|
226
|
+
}
|
|
227
|
+
>();
|
|
228
|
+
|
|
229
|
+
async function loadFrontendDocument(
|
|
230
|
+
workspaceRoot: string,
|
|
231
|
+
pathname: string,
|
|
232
|
+
): Promise<{
|
|
233
|
+
path: string;
|
|
234
|
+
html: string;
|
|
235
|
+
cacheStatus: RequestTimeDocumentCacheStatus;
|
|
236
|
+
}> {
|
|
237
|
+
const documentPath = await resolveFrontendDocumentPath(workspaceRoot, pathname);
|
|
238
|
+
const documentStats = await stat(documentPath);
|
|
239
|
+
const cached = documentTemplateCache.get(documentPath);
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
cached &&
|
|
243
|
+
cached.ctimeMs === documentStats.ctimeMs &&
|
|
244
|
+
cached.mtimeMs === documentStats.mtimeMs &&
|
|
245
|
+
cached.size === documentStats.size
|
|
246
|
+
) {
|
|
247
|
+
return {
|
|
248
|
+
path: documentPath,
|
|
249
|
+
html: cached.html,
|
|
250
|
+
cacheStatus: 'hit',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const html = await readTextFile(documentPath);
|
|
255
|
+
documentTemplateCache.set(documentPath, {
|
|
256
|
+
html,
|
|
257
|
+
ctimeMs: documentStats.ctimeMs,
|
|
258
|
+
mtimeMs: documentStats.mtimeMs,
|
|
259
|
+
size: documentStats.size,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
path: documentPath,
|
|
264
|
+
html,
|
|
265
|
+
cacheStatus: cached ? 'stale' : 'miss',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function resolveFrontendDocumentPath(
|
|
270
|
+
workspaceRoot: string,
|
|
271
|
+
pathname: string,
|
|
272
|
+
): Promise<string> {
|
|
273
|
+
const candidates = getFrontendDocumentCandidates(workspaceRoot, pathname);
|
|
274
|
+
|
|
275
|
+
for (const candidate of candidates) {
|
|
276
|
+
if (await fileExists(candidate)) {
|
|
277
|
+
return candidate;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Frontend document for ${normalizePath(pathname)} was not found. Checked ${candidates.join(', ')}.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getFrontendDocumentCandidates(workspaceRoot: string, pathname: string): string[] {
|
|
287
|
+
const pageName = firstPathSegment(pathname) ?? 'home';
|
|
288
|
+
const relativeCandidates =
|
|
289
|
+
pageName === 'home'
|
|
290
|
+
? [
|
|
291
|
+
path.join('pages', 'home', 'index.html'),
|
|
292
|
+
path.join('home', 'index.html'),
|
|
293
|
+
'home.html',
|
|
294
|
+
'index.html',
|
|
295
|
+
]
|
|
296
|
+
: [
|
|
297
|
+
path.join('pages', pageName, 'index.html'),
|
|
298
|
+
path.join(pageName, 'index.html'),
|
|
299
|
+
`${pageName}.html`,
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const candidates = [
|
|
303
|
+
...relativeCandidates.map((relativePath) =>
|
|
304
|
+
path.join(workspaceRoot, 'build', 'frontend', relativePath),
|
|
305
|
+
),
|
|
306
|
+
...relativeCandidates.map((relativePath) =>
|
|
307
|
+
path.join(workspaceRoot, 'dist', 'frontend', relativePath),
|
|
308
|
+
),
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
return Array.from(new Set(candidates));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function fileExists(targetPath: string): Promise<boolean> {
|
|
315
|
+
try {
|
|
316
|
+
await access(targetPath);
|
|
317
|
+
return true;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function injectViewState(
|
|
324
|
+
documentHtml: string,
|
|
325
|
+
state: {
|
|
326
|
+
name: string;
|
|
327
|
+
templatePath: string;
|
|
328
|
+
pathname: string;
|
|
329
|
+
params: Record<string, string>;
|
|
330
|
+
data: unknown;
|
|
331
|
+
requestId?: string;
|
|
332
|
+
},
|
|
333
|
+
): string {
|
|
334
|
+
const payload = serializeJsonForHtml({
|
|
335
|
+
view: {
|
|
336
|
+
name: state.name,
|
|
337
|
+
path: state.templatePath,
|
|
338
|
+
pathname: state.pathname,
|
|
339
|
+
params: state.params,
|
|
340
|
+
},
|
|
341
|
+
data: state.data,
|
|
342
|
+
requestId: state.requestId ?? null,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const scriptTag = `<script type="application/json" id="webstir-view-state">${payload}</script>`;
|
|
346
|
+
const htmlWithBodyAttributes = documentHtml.replace(
|
|
347
|
+
/<body\b([^>]*)>/i,
|
|
348
|
+
(_match, existingAttributes) => {
|
|
349
|
+
const attrs = [
|
|
350
|
+
`data-webstir-view-name="${escapeHtmlAttribute(state.name)}"`,
|
|
351
|
+
`data-webstir-view-pathname="${escapeHtmlAttribute(state.pathname)}"`,
|
|
352
|
+
`data-webstir-view-template="${escapeHtmlAttribute(state.templatePath)}"`,
|
|
353
|
+
];
|
|
354
|
+
return `<body${existingAttributes} ${attrs.join(' ')}>`;
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (/<\/body>/i.test(htmlWithBodyAttributes)) {
|
|
359
|
+
return htmlWithBodyAttributes.replace(/<\/body>/i, `${scriptTag}\n</body>`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (/<\/html>/i.test(htmlWithBodyAttributes)) {
|
|
363
|
+
return htmlWithBodyAttributes.replace(/<\/html>/i, `${scriptTag}\n</html>`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return `${htmlWithBodyAttributes}\n${scriptTag}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function serializeJsonForHtml(value: unknown): string {
|
|
370
|
+
return JSON.stringify(value)
|
|
371
|
+
.replace(/</g, '\\u003c')
|
|
372
|
+
.replace(/>/g, '\\u003e')
|
|
373
|
+
.replace(/&/g, '\\u0026')
|
|
374
|
+
.replace(/\u2028/g, '\\u2028')
|
|
375
|
+
.replace(/\u2029/g, '\\u2029');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function escapeHtmlAttribute(value: string): string {
|
|
379
|
+
return value
|
|
380
|
+
.replaceAll('&', '&')
|
|
381
|
+
.replaceAll('"', '"')
|
|
382
|
+
.replaceAll("'", ''')
|
|
383
|
+
.replaceAll('<', '<')
|
|
384
|
+
.replaceAll('>', '>');
|
|
385
|
+
}
|
package/src/scaffold/assets.ts
CHANGED
|
@@ -4,78 +4,82 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import type { ModuleAsset } from '@webstir-io/module-contract';
|
|
5
5
|
|
|
6
6
|
export async function getBackendScaffoldAssets(): Promise<readonly ModuleAsset[]> {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const packageRoot = path.resolve(here, '..', '..');
|
|
9
|
+
const templatesRoot = path.join(packageRoot, 'templates', 'backend');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
sourcePath: path.join(templatesRoot, 'tsconfig.json'),
|
|
14
|
+
targetPath: path.join('src', 'backend', 'tsconfig.json'),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
sourcePath: path.join(templatesRoot, 'index.ts'),
|
|
18
|
+
targetPath: path.join('src', 'backend', 'index.ts'),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
sourcePath: path.join(templatesRoot, 'module.ts'),
|
|
22
|
+
targetPath: path.join('src', 'backend', 'module.ts'),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
sourcePath: path.join(templatesRoot, 'auth', 'adapter.ts'),
|
|
26
|
+
targetPath: path.join('src', 'backend', 'auth', 'adapter.ts'),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
sourcePath: path.join(templatesRoot, 'observability', 'logger.ts'),
|
|
30
|
+
targetPath: path.join('src', 'backend', 'observability', 'logger.ts'),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
sourcePath: path.join(templatesRoot, 'observability', 'metrics.ts'),
|
|
34
|
+
targetPath: path.join('src', 'backend', 'observability', 'metrics.ts'),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
sourcePath: path.join(templatesRoot, 'env.ts'),
|
|
38
|
+
targetPath: path.join('src', 'backend', 'env.ts'),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
sourcePath: path.join(templatesRoot, 'session', 'store.ts'),
|
|
42
|
+
targetPath: path.join('src', 'backend', 'session', 'store.ts'),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
sourcePath: path.join(templatesRoot, 'session', 'sqlite.ts'),
|
|
46
|
+
targetPath: path.join('src', 'backend', 'session', 'sqlite.ts'),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
sourcePath: path.join(templatesRoot, 'functions', 'hello', 'index.ts'),
|
|
50
|
+
targetPath: path.join('src', 'backend', 'functions', 'hello', 'index.ts'),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
sourcePath: path.join(templatesRoot, 'jobs', 'nightly', 'index.ts'),
|
|
54
|
+
targetPath: path.join('src', 'backend', 'jobs', 'nightly', 'index.ts'),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
sourcePath: path.join(templatesRoot, 'jobs', 'runtime.ts'),
|
|
58
|
+
targetPath: path.join('src', 'backend', 'jobs', 'runtime.ts'),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
sourcePath: path.join(templatesRoot, 'jobs', 'scheduler.ts'),
|
|
62
|
+
targetPath: path.join('src', 'backend', 'jobs', 'scheduler.ts'),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
sourcePath: path.join(templatesRoot, 'db', 'connection.ts'),
|
|
66
|
+
targetPath: path.join('src', 'backend', 'db', 'connection.ts'),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
sourcePath: path.join(templatesRoot, 'db', 'migrate.ts'),
|
|
70
|
+
targetPath: path.join('src', 'backend', 'db', 'migrate.ts'),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
sourcePath: path.join(templatesRoot, 'db', 'migrations', '0001-example.ts'),
|
|
74
|
+
targetPath: path.join('src', 'backend', 'db', 'migrations', '0001-example.ts'),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
sourcePath: path.join(templatesRoot, 'db', 'types.d.ts'),
|
|
78
|
+
targetPath: path.join('src', 'backend', 'db', 'types.d.ts'),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
sourcePath: path.join(templatesRoot, '.env.example'),
|
|
82
|
+
targetPath: path.join('.env.example'),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
81
85
|
}
|
package/src/testing/context.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
const GLOBAL_KEY = '__webstirBackendTestContext__';
|
|
2
2
|
export function setBackendTestContext(context) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
3
|
+
const target = globalThis;
|
|
4
|
+
if (context) {
|
|
5
|
+
target[GLOBAL_KEY] = context;
|
|
6
|
+
} else {
|
|
7
|
+
delete target[GLOBAL_KEY];
|
|
8
|
+
}
|
|
10
9
|
}
|
|
11
10
|
export function getBackendTestContext() {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const target = globalThis;
|
|
12
|
+
return target[GLOBAL_KEY] ?? null;
|
|
14
13
|
}
|
package/src/testing/context.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { BackendTestContext } from './types.js';
|
|
2
2
|
|
|
3
|
-
const GLOBAL_KEY = '
|
|
3
|
+
const GLOBAL_KEY = Symbol.for('webstir.backendTestContext');
|
|
4
4
|
|
|
5
5
|
export function setBackendTestContext(context: BackendTestContext | null): void {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
const target = globalThis as Record<string | symbol, unknown>;
|
|
7
|
+
if (context) {
|
|
8
|
+
target[GLOBAL_KEY] = context;
|
|
9
|
+
} else {
|
|
10
|
+
delete target[GLOBAL_KEY];
|
|
11
|
+
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function getBackendTestContext(): BackendTestContext | null {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const target = globalThis as Record<string | symbol, unknown>;
|
|
16
|
+
return (target[GLOBAL_KEY] ?? null) as BackendTestContext | null;
|
|
17
17
|
}
|
package/src/testing/index.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { getBackendTestContext, setBackendTestContext } from './context.js';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
2
|
+
import type {
|
|
3
|
+
BackendTestCallback,
|
|
4
|
+
BackendTestHarness,
|
|
5
|
+
BackendTestHarnessOptions,
|
|
6
|
+
} from './types.js';
|
|
7
|
+
export type {
|
|
8
|
+
BackendTestCallback,
|
|
9
|
+
BackendTestContext,
|
|
10
|
+
BackendTestHarness,
|
|
11
|
+
BackendTestHarnessOptions,
|
|
12
|
+
} from './types.js';
|
|
4
13
|
export { getBackendTestContext, setBackendTestContext };
|
|
5
|
-
export declare function createBackendTestHarness(
|
|
14
|
+
export declare function createBackendTestHarness(
|
|
15
|
+
options?: BackendTestHarnessOptions,
|
|
16
|
+
): Promise<BackendTestHarness>;
|
|
6
17
|
export declare function backendTest(name: string, callback: BackendTestCallback): void;
|