@torpor/build 0.4.14 → 1.0.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 +20 -2
- package/dist/Jsonify-Dg6gChd3.d.mts +10 -0
- package/dist/Jsonify-Dg6gChd3.d.mts.map +1 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts +42 -0
- package/dist/ParseRouteParams-DBmqlmLh.d.mts.map +1 -0
- package/dist/{RouteType-B6BMOyYw.mjs → Router-C4ADUfhv.mjs} +30 -42
- package/dist/Router-C4ADUfhv.mjs.map +1 -0
- package/dist/{Server-CF5wDJp6.d.mts → Server-C_IKav_e.d.mts} +44 -16
- package/dist/Server-C_IKav_e.d.mts.map +1 -0
- package/dist/{ServerEvent-CxPwA4lH.mjs → ServerEvent-Dljn5i7A.mjs} +37 -11
- package/dist/ServerEvent-Dljn5i7A.mjs.map +1 -0
- package/dist/Site-CMa153FA.d.mts +565 -0
- package/dist/Site-CMa153FA.d.mts.map +1 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts +100 -0
- package/dist/StandardSchema-D28L8_zZ.d.mts.map +1 -0
- package/dist/TypedResponse-6lznqN4U.d.mts +22 -0
- package/dist/TypedResponse-6lznqN4U.d.mts.map +1 -0
- package/dist/{_page-BQs4WvCO.mjs → _page-4cQKs_Dn.mjs} +3 -5
- package/dist/_page-4cQKs_Dn.mjs.map +1 -0
- package/dist/bin/index.d.mts +1 -1
- package/dist/bin/index.js +15 -11
- package/dist/bin/index.js.map +1 -1
- package/dist/flattenHeaders-C_YYLdOq.mjs +114 -0
- package/dist/flattenHeaders-C_YYLdOq.mjs.map +1 -0
- package/dist/form.d.mts +35 -0
- package/dist/form.d.mts.map +1 -0
- package/dist/form.mjs +48 -0
- package/dist/form.mjs.map +1 -0
- package/dist/index.d.mts +72 -156
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +158 -26
- package/dist/index.mjs.map +1 -1
- package/dist/nav.d.mts +58 -5
- package/dist/nav.d.mts.map +1 -1
- package/dist/nav.mjs +116 -38
- package/dist/nav.mjs.map +1 -1
- package/dist/openapi.d.mts +144 -0
- package/dist/openapi.d.mts.map +1 -0
- package/dist/openapi.mjs +2 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs +160 -0
- package/dist/pathTrie-D4Ax2Mu9.mjs.map +1 -0
- package/dist/plugin-D8Www11B.mjs +202 -0
- package/dist/plugin-D8Www11B.mjs.map +1 -0
- package/dist/{response-C5TtAsh1.mjs → response-Bg4w2S1q.mjs} +16 -39
- package/dist/response-Bg4w2S1q.mjs.map +1 -0
- package/dist/response.d.mts +309 -289
- package/dist/response.d.mts.map +1 -1
- package/dist/response.mjs +3 -4
- package/dist/run.d.mts +11 -4
- package/dist/run.d.mts.map +1 -1
- package/dist/run.mjs +2 -8
- package/dist/runOpenApi-BUi1RH1l.mjs +1381 -0
- package/dist/runOpenApi-BUi1RH1l.mjs.map +1 -0
- package/dist/schema.d.mts +32 -0
- package/dist/schema.d.mts.map +1 -0
- package/dist/schema.mjs +2 -0
- package/dist/server.d.mts +10 -8
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +132 -4
- package/dist/server.mjs.map +1 -0
- package/dist/state.d.mts.map +1 -1
- package/dist/state.mjs +2 -3
- package/dist/test.d.mts +8 -10
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +283 -79
- package/dist/test.mjs.map +1 -1
- package/dist/{seeOther-B4Yhu9iq.mjs → unprocessable-DAZbrDeR.mjs} +34 -13
- package/dist/unprocessable-DAZbrDeR.mjs.map +1 -0
- package/dist/validate-Ok4krlkX.mjs +40 -0
- package/dist/validate-Ok4krlkX.mjs.map +1 -0
- package/package.json +28 -17
- package/src/bin/index.ts +13 -2
- package/src/dev.ts +10 -0
- package/src/form/formDataToRecord.ts +23 -0
- package/src/form/readForm.ts +71 -0
- package/src/form.ts +4 -0
- package/src/index.ts +53 -1
- package/src/nav/api.test-d.ts +79 -0
- package/src/nav/api.ts +104 -0
- package/src/nav/formSubmit.ts +18 -4
- package/src/nav/navigate.ts +40 -15
- package/src/nav/route.ts +38 -0
- package/src/nav.ts +3 -1
- package/src/openapi/docsHtml.ts +25 -0
- package/src/openapi/document.ts +154 -0
- package/src/openapi/plugin.ts +78 -0
- package/src/openapi/types.ts +82 -0
- package/src/openapi.ts +14 -0
- package/src/response/TypedResponse.ts +17 -0
- package/src/response/badRequest.ts +14 -2
- package/src/response/created.ts +13 -2
- package/src/response/found.ts +2 -2
- package/src/response/movedPermanently.ts +2 -2
- package/src/response/notModified.ts +2 -2
- package/src/response/ok.ts +13 -4
- package/src/response/response.ts +4 -4
- package/src/response/unprocessable.ts +14 -2
- package/src/run/depCache.ts +175 -0
- package/src/run/devPlugin.ts +128 -0
- package/src/run/prepareTemplate.ts +6 -3
- package/src/run/run.ts +63 -39
- package/src/run/runBuild.ts +63 -12
- package/src/run/runDev.ts +146 -58
- package/src/run/runOpenApi.ts +52 -0
- package/src/run/runPreview.ts +17 -26
- package/src/run/staleTorpCopies.ts +100 -0
- package/src/run.ts +2 -1
- package/src/schema.ts +7 -0
- package/src/server/CookieHelper.ts +17 -7
- package/src/server/Server.ts +54 -30
- package/src/server/ServerEvent.ts +24 -1
- package/src/server/connect/connectMiddleware.ts +44 -40
- package/src/server/connect/flattenHeaders.ts +1 -23
- package/src/server/connect/requestToNodeMessage.ts +6 -3
- package/src/server/types/MiddlewareFunction.ts +18 -4
- package/src/site/Router.ts +52 -41
- package/src/site/Site.ts +184 -10
- package/src/site/checkLayoutSlots.ts +114 -0
- package/src/site/checkRoutes.ts +435 -0
- package/src/site/clientEntry.ts +22 -9
- package/src/site/layoutSlots.ts +53 -0
- package/src/site/manifest.ts +125 -6
- package/src/site/serverEntry.ts +341 -132
- package/src/state/$page.ts +2 -1
- package/src/state/$serverPage.ts +22 -0
- package/src/test/runTest.ts +306 -118
- package/src/types/Adapter.ts +12 -0
- package/src/types/Jsonify.ts +20 -0
- package/src/types/PageData.test-d.ts +136 -0
- package/src/types/PageData.ts +35 -0
- package/src/types/PageEndPoint.ts +25 -9
- package/src/types/PageForm.test-d.ts +78 -0
- package/src/types/PageForm.ts +29 -0
- package/src/types/PageLoadEvent.ts +16 -4
- package/src/types/PageLoadReturn.ts +13 -0
- package/src/types/PageProps.ts +14 -0
- package/src/types/PageServerAction.ts +8 -2
- package/src/types/PageServerEndPoint.test-d.ts +105 -0
- package/src/types/PageServerEndPoint.ts +99 -5
- package/src/types/PageServerLoad.ts +11 -3
- package/src/types/ParseRouteParams.test-d.ts +84 -0
- package/src/types/ParseRouteParams.ts +64 -0
- package/src/types/Route.ts +20 -1
- package/src/types/RouteHandler.ts +8 -1
- package/src/types/ServerEndPoint.test-d.ts +77 -0
- package/src/types/ServerEndPoint.ts +148 -16
- package/src/types/ServerHook.ts +10 -3
- package/src/types/ServerLoadEvent.ts +48 -3
- package/src/types/ServerRequest.ts +10 -2
- package/src/types/SitePlugin.ts +24 -0
- package/src/types/StandardSchema.ts +109 -0
- package/src/utils/pathToRegex.ts +4 -2
- package/src/utils/pathTrie.ts +182 -0
- package/src/utils/searchParamsToRecord.ts +18 -0
- package/src/utils/torporPackages.ts +150 -0
- package/src/utils/tsconfigAliases.ts +90 -0
- package/src/validation/ValidationError.ts +18 -0
- package/src/validation/endpoint.ts +83 -0
- package/src/validation/validate.ts +26 -0
- package/dist/RouteType-B6BMOyYw.mjs.map +0 -1
- package/dist/Server-CF5wDJp6.d.mts.map +0 -1
- package/dist/ServerEvent-CxPwA4lH.mjs.map +0 -1
- package/dist/Site-DgC6WWn1.d.mts +0 -78
- package/dist/Site-DgC6WWn1.d.mts.map +0 -1
- package/dist/_page-BQs4WvCO.mjs.map +0 -1
- package/dist/connectMiddleware-D7nehjj3.mjs +0 -252
- package/dist/connectMiddleware-D7nehjj3.mjs.map +0 -1
- package/dist/pathToRegex-TUAMc3zS.mjs +0 -11
- package/dist/pathToRegex-TUAMc3zS.mjs.map +0 -1
- package/dist/response-C5TtAsh1.mjs.map +0 -1
- package/dist/run-BPFtSYA3.mjs +0 -332
- package/dist/run-BPFtSYA3.mjs.map +0 -1
- package/dist/seeOther-B4Yhu9iq.mjs.map +0 -1
- package/src/server/Routerx.ts +0 -72
- package/src/server/connect/bufferToArrayBuffer.ts +0 -8
- package/src/server/connect/readableToBuffer.ts +0 -16
package/src/run/runDev.ts
CHANGED
|
@@ -1,26 +1,67 @@
|
|
|
1
1
|
import estorpor from "@torpor/unplugin/esbuild";
|
|
2
2
|
import torpor from "@torpor/unplugin/vite";
|
|
3
3
|
import { configDotenv } from "dotenv";
|
|
4
|
-
import {
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import {
|
|
7
|
+
type AliasOptions,
|
|
8
|
+
createServer as createViteServer,
|
|
9
|
+
type Plugin,
|
|
10
|
+
type ViteDevServer,
|
|
11
|
+
} from "vite";
|
|
10
12
|
import Site from "../site/Site.ts";
|
|
13
|
+
import {
|
|
14
|
+
checkApiCallSource,
|
|
15
|
+
checkApiCalls,
|
|
16
|
+
checkRoute,
|
|
17
|
+
checkRoutes,
|
|
18
|
+
createApiCallCheck,
|
|
19
|
+
type RouteCheckIssue,
|
|
20
|
+
reportRouteIssues,
|
|
21
|
+
} from "../site/checkRoutes";
|
|
11
22
|
import manifest from "../site/manifest.ts";
|
|
12
|
-
import
|
|
23
|
+
import { checkLayoutSlot, checkLayoutSlots } from "../site/checkLayoutSlots";
|
|
24
|
+
import { LAYOUT_ROUTE } from "../types/RouteType";
|
|
25
|
+
import tsconfigAliases, { type AliasEntry } from "../utils/tsconfigAliases";
|
|
26
|
+
import { addTorporPackageConfig } from "../utils/torporPackages";
|
|
27
|
+
import devPlugin from "./devPlugin.ts";
|
|
28
|
+
import { clearStaleDepCache } from "./depCache";
|
|
29
|
+
import { reportStaleTorpCopies } from "./staleTorpCopies";
|
|
13
30
|
|
|
14
31
|
export default async function runDev(site: Site): Promise<void> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
// "custom", disabling Vite's own HTML serving logic so the parent server
|
|
19
|
-
// can take control
|
|
32
|
+
// Create the Vite dev server. Unlike the previous middleware-mode setup, we
|
|
33
|
+
// let Vite own the HTTP server (so HMR/websockets work natively) and let the
|
|
34
|
+
// adapter's `dev()` plugin(s) handle SSR via configureServer.
|
|
20
35
|
const config = structuredClone(site.viteConfig ?? {});
|
|
21
|
-
config.server = { middlewareMode: true };
|
|
22
36
|
config.appType = "custom";
|
|
23
|
-
config.
|
|
37
|
+
config.server ??= {};
|
|
38
|
+
config.server.host ??= "localhost";
|
|
39
|
+
config.server.port ??= 7059;
|
|
40
|
+
// Resolve tsconfig path aliases (e.g. `@/*`). Previously provided by the
|
|
41
|
+
// default `vite-tsconfig-paths` plugin on Site; vite-plus handles it inline
|
|
42
|
+
config.resolve ??= {};
|
|
43
|
+
config.resolve.tsconfigPaths ??= true;
|
|
44
|
+
// Resolve `@torpor/view` (runtime, compiler, SSR) from source in dev mode
|
|
45
|
+
// so changes to the compiler take effect without rebuilding dist.
|
|
46
|
+
config.resolve.conditions ??= [];
|
|
47
|
+
config.resolve.conditions.push("development");
|
|
48
|
+
// vite-plus' `tsconfigPaths` isn't honored by the SSR module-runner's
|
|
49
|
+
// externalization path (it hardcodes tsconfigPaths:false), so dev SSR can't
|
|
50
|
+
// resolve path aliases. Mirror tsconfig `compilerOptions.paths` as Vite
|
|
51
|
+
// `resolve.alias` so they resolve during transform for both client and SSR.
|
|
52
|
+
config.resolve.alias = [...asAliasArray(config.resolve.alias), ...tsconfigAliases(site.root)];
|
|
53
|
+
|
|
54
|
+
// The adapter provides the dev-runtime plugin(s); fall back to the
|
|
55
|
+
// framework's Node-runtime plugin when it doesn't.
|
|
56
|
+
const adapterDev = site.adapter.dev?.(site);
|
|
57
|
+
const devPlugins = normalizePlugins(adapterDev ?? devPlugin(site));
|
|
58
|
+
|
|
59
|
+
config.plugins = [
|
|
60
|
+
manifest(site, true),
|
|
61
|
+
torpor({ dev: true }),
|
|
62
|
+
...devPlugins,
|
|
63
|
+
...site.vitePlugins,
|
|
64
|
+
];
|
|
24
65
|
|
|
25
66
|
// HACK: To be able to import `.torp` files from barrel files in
|
|
26
67
|
// node_modules, we need to add their libraries to `ssr.noExternal` in
|
|
@@ -28,62 +69,109 @@ export default async function runDev(site: Site): Promise<void> {
|
|
|
28
69
|
config.optimizeDeps ??= {};
|
|
29
70
|
config.optimizeDeps.extensions ??= [];
|
|
30
71
|
config.optimizeDeps.extensions.push(".torp");
|
|
31
|
-
config.optimizeDeps.
|
|
32
|
-
config.optimizeDeps.
|
|
33
|
-
config.optimizeDeps.
|
|
72
|
+
config.optimizeDeps.rolldownOptions ??= {};
|
|
73
|
+
config.optimizeDeps.rolldownOptions.plugins ??= [estorpor()];
|
|
74
|
+
// TODO: config.optimizeDeps.rolldownOptions.plugins.push(estorpor());
|
|
75
|
+
|
|
76
|
+
// Packages that ship `.torp` files (e.g. icon libraries) need the torpor
|
|
77
|
+
// compiler: dep optimization would parse them as plain JavaScript and
|
|
78
|
+
// fail, and SSR externalization would leave raw `.torp` imports that
|
|
79
|
+
// Node/workerd can't load. Exclude them from optimization and mark them
|
|
80
|
+
// for SSR bundling so they go through the torpor plugin
|
|
81
|
+
addTorporPackageConfig(site.root, config);
|
|
82
|
+
|
|
83
|
+
// Workspace packages may have been rebuilt since the last dev run; drop
|
|
84
|
+
// Vite's dep cache so SSR never executes stale prebundled modules
|
|
85
|
+
clearStaleDepCache(site.root, (message) => console.log(`\n${message}`));
|
|
86
|
+
|
|
87
|
+
// A workspace package with dist .torp copies (e.g. @torpor/ui) whose src
|
|
88
|
+
// changed without a rebuild serves OLD components to the site, silently;
|
|
89
|
+
// report the drift so it can be fixed with a rebuild
|
|
90
|
+
reportStaleTorpCopies(site.root, (message) => console.log(`\n${message}`));
|
|
91
|
+
|
|
92
|
+
// Load environment variables from a `.env` file, with defaults if not set
|
|
93
|
+
configDotenv();
|
|
94
|
+
|
|
95
|
+
process.env.PROTOCOL ??= "http:";
|
|
96
|
+
process.env.HOST ??= "localhost";
|
|
97
|
+
process.env.PORT ??= "7059";
|
|
98
|
+
|
|
99
|
+
const connectingUrl = `${process.env.PROTOCOL}//${process.env.HOST}:${process.env.PORT}`;
|
|
100
|
+
console.log(`\nConnecting to ${connectingUrl}`);
|
|
34
101
|
|
|
35
102
|
const vite = await createViteServer(config);
|
|
103
|
+
await vite.listen();
|
|
36
104
|
|
|
37
|
-
|
|
38
|
-
// It's called site.html because @torpor/build builds the site (html, routes
|
|
39
|
-
// etc) while the user builds the app (components etc)
|
|
40
|
-
const templateFile = path.resolve(site.root, "src/site.html");
|
|
41
|
-
let template = await fs.readFile(templateFile, "utf-8");
|
|
105
|
+
watchRouteTypes(site, vite);
|
|
42
106
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
template = await vite.transformIndexHtml("", template);
|
|
107
|
+
const listeningUrl = vite.resolvedUrls?.local?.[0] ?? connectingUrl;
|
|
108
|
+
console.log(`Listening on ${listeningUrl}\n`);
|
|
109
|
+
}
|
|
47
110
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Reports route type issues at startup, then re-checks route files and
|
|
113
|
+
* `makeApi` calls as they change so annotation problems surface during
|
|
114
|
+
* development without failing the dev server.
|
|
115
|
+
*/
|
|
116
|
+
function watchRouteTypes(site: Site, vite: ViteDevServer): void {
|
|
117
|
+
reportRouteIssues(checkRoutes(site));
|
|
118
|
+
reportRouteIssues(checkApiCalls(site));
|
|
119
|
+
reportRouteIssues(checkLayoutSlots(site));
|
|
52
120
|
|
|
53
|
-
//
|
|
54
|
-
|
|
121
|
+
// Map absolute route file paths to their manifest entries, so changed
|
|
122
|
+
// files can be re-checked against their derived route. Layout route files
|
|
123
|
+
// are included for their slot check (a .torp layout is its own component)
|
|
124
|
+
const routeFiles = new Map(
|
|
125
|
+
site.routes
|
|
126
|
+
.filter(
|
|
127
|
+
(r) => r.file?.endsWith(".ts") || (r.file?.endsWith(".torp") && r.type === LAYOUT_ROUTE),
|
|
128
|
+
)
|
|
129
|
+
.map((r) => [path.resolve(site.root, r.file!), r]),
|
|
130
|
+
);
|
|
131
|
+
const apiCheck = createApiCallCheck(site);
|
|
132
|
+
// Only report when a file's issues change, to avoid repeating the same
|
|
133
|
+
// warnings on every save
|
|
134
|
+
const reported = new Map<string, string>();
|
|
55
135
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
136
|
+
const report = (file: string, issues: RouteCheckIssue[]) => {
|
|
137
|
+
const key = JSON.stringify(issues);
|
|
138
|
+
if (reported.get(file) === key) return;
|
|
139
|
+
reported.set(file, key);
|
|
140
|
+
reportRouteIssues(issues);
|
|
141
|
+
};
|
|
60
142
|
|
|
61
|
-
|
|
62
|
-
|
|
143
|
+
const recheck = (file: string): void => {
|
|
144
|
+
const route = routeFiles.get(file);
|
|
145
|
+
if (route) {
|
|
146
|
+
report(
|
|
147
|
+
file,
|
|
148
|
+
route.type === LAYOUT_ROUTE ? checkLayoutSlot(site, route) : checkRoute(site, route),
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!/\.(ts|js|torp)$/.test(file)) return;
|
|
153
|
+
let source: string;
|
|
63
154
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const { load } = await vite.ssrLoadModule(serverScript);
|
|
68
|
-
|
|
69
|
-
// Render the app HTML (or fetch server data etc) via serverEntry's
|
|
70
|
-
// exported `load` function
|
|
71
|
-
return await load(ev, template);
|
|
72
|
-
} catch (e: any) {
|
|
73
|
-
// If an error is caught, let Vite fix the stack trace so it maps
|
|
74
|
-
// back to your actual source code
|
|
75
|
-
vite.ssrFixStacktrace(e);
|
|
76
|
-
return serverError(e);
|
|
155
|
+
source = readFileSync(file, "utf8");
|
|
156
|
+
} catch {
|
|
157
|
+
return;
|
|
77
158
|
}
|
|
78
|
-
|
|
159
|
+
report(file, checkApiCallSource(source, file, apiCheck));
|
|
160
|
+
};
|
|
79
161
|
|
|
80
|
-
|
|
81
|
-
|
|
162
|
+
vite.watcher.on("change", recheck);
|
|
163
|
+
vite.watcher.on("add", recheck);
|
|
164
|
+
}
|
|
82
165
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
166
|
+
function normalizePlugins(plugins: Plugin | Plugin[] | void): Plugin[] {
|
|
167
|
+
if (!plugins) return [];
|
|
168
|
+
return Array.isArray(plugins) ? plugins : [plugins];
|
|
169
|
+
}
|
|
86
170
|
|
|
87
|
-
|
|
88
|
-
|
|
171
|
+
function asAliasArray(alias: AliasOptions | undefined): AliasEntry[] {
|
|
172
|
+
// Normalize an existing `resolve.alias` value (array | object | undefined)
|
|
173
|
+
// into an array so we can concatenate our tsconfig-derived aliases.
|
|
174
|
+
if (!alias) return [];
|
|
175
|
+
if (Array.isArray(alias)) return alias;
|
|
176
|
+
return Object.entries(alias).map(([find, replacement]) => ({ find, replacement }));
|
|
89
177
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ViteDevServer } from "vite";
|
|
4
|
+
import buildOpenApiDocument from "../openapi/document";
|
|
5
|
+
import { OPEN_API_STATE_KEY } from "../openapi/plugin";
|
|
6
|
+
import type { OpenApiEndPoint, OpenApiRouteEntry, ResolvedOpenApiOptions } from "../openapi/types";
|
|
7
|
+
import type Site from "../site/Site";
|
|
8
|
+
import { SERVER_ROUTE } from "../types/RouteType";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Writes an OpenAPI document for the site's +server endpoints to a file.
|
|
12
|
+
* Route files are loaded through the supplied Vite server, so this must be
|
|
13
|
+
* called while the server from `loadSite` is still open.
|
|
14
|
+
*/
|
|
15
|
+
export default async function runOpenApi(
|
|
16
|
+
site: Site,
|
|
17
|
+
vite: ViteDevServer,
|
|
18
|
+
outFile = "openapi.json",
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const options = site.pluginState.get(OPEN_API_STATE_KEY) as ResolvedOpenApiOptions | undefined;
|
|
21
|
+
if (!options) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'No OpenAPI options found. Add the openApi() plugin to site.plugins to use "tb --openapi"',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const entries: OpenApiRouteEntry[] = [];
|
|
28
|
+
for (const route of site.routes) {
|
|
29
|
+
if (route.type !== SERVER_ROUTE) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Don't document the docs page itself
|
|
33
|
+
if (options.docs && route.path === options.docs) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (route.endPoint && !route.file) {
|
|
37
|
+
// Inline endpoint
|
|
38
|
+
entries.push({ path: route.path, endPoint: route.endPoint as OpenApiEndPoint });
|
|
39
|
+
} else if (route.file) {
|
|
40
|
+
const file = path.resolve(site.root, route.file);
|
|
41
|
+
const mod = await vite.ssrLoadModule(file);
|
|
42
|
+
entries.push({ path: route.path, endPoint: mod.default });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const doc = buildOpenApiDocument(entries, options);
|
|
47
|
+
await writeFile(path.resolve(site.root, outFile), JSON.stringify(doc, null, 2) + "\n");
|
|
48
|
+
|
|
49
|
+
console.log(
|
|
50
|
+
`OpenAPI document written to ${outFile} (${Object.keys(doc.paths ?? {}).length} paths)`,
|
|
51
|
+
);
|
|
52
|
+
}
|
package/src/run/runPreview.ts
CHANGED
|
@@ -16,23 +16,24 @@ export default async function runPreview(site: Site): Promise<void> {
|
|
|
16
16
|
const clientFolder = path.join(distFolder, "client");
|
|
17
17
|
const serverFolder = path.join(distFolder, "server");
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
);
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
// Read site.html if present, and find the client script file in /assets.
|
|
20
|
+
// An endpoints-only site has no site.html, and no template is needed
|
|
21
|
+
let template: string | undefined;
|
|
22
|
+
const siteHtml = path.join(clientFolder, "site.html");
|
|
23
|
+
if (existsSync(siteHtml)) {
|
|
24
|
+
let clientScript = (await fs.readdir(path.join(clientFolder, "assets"))).find((f) =>
|
|
25
|
+
f.startsWith("clientEntry-"),
|
|
26
|
+
);
|
|
27
|
+
if (!clientScript) {
|
|
28
|
+
throw new Error("clientEntry.js not found");
|
|
29
|
+
}
|
|
30
|
+
clientScript = `/assets/${clientScript}`;
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
// It's called site.html because @torpor/build builds the site (html, routes
|
|
30
|
-
// etc) while the user builds the app (components etc)
|
|
31
|
-
let siteHtml = path.join(clientFolder, "site.html");
|
|
32
|
-
let template = await fs.readFile(siteHtml, "utf-8");
|
|
32
|
+
template = await fs.readFile(siteHtml, "utf-8");
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Prepare site.html so that we can just splice components into it
|
|
35
|
+
template = prepareTemplate(template, clientScript);
|
|
36
|
+
}
|
|
36
37
|
|
|
37
38
|
// Configure the server
|
|
38
39
|
const server = new Server();
|
|
@@ -43,8 +44,7 @@ export default async function runPreview(site: Site): Promise<void> {
|
|
|
43
44
|
const url = new URL(ev.request.url);
|
|
44
45
|
const file = path.join(site.root, "dist", "client", url.pathname);
|
|
45
46
|
if (existsSync(file)) {
|
|
46
|
-
|
|
47
|
-
const body = bufferToArrayBuffer(await fs.readFile(file));
|
|
47
|
+
const body = await fs.readFile(file);
|
|
48
48
|
return new Response(body, {
|
|
49
49
|
status: 200,
|
|
50
50
|
headers: {
|
|
@@ -82,12 +82,3 @@ export default async function runPreview(site: Site): Promise<void> {
|
|
|
82
82
|
// Serve the site
|
|
83
83
|
site.adapter.serve(server, site);
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
// From https://stackoverflow.com/a/79345620
|
|
87
|
-
// No idea why it is needed
|
|
88
|
-
function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer {
|
|
89
|
-
return buffer.buffer.slice(
|
|
90
|
-
buffer.byteOffset,
|
|
91
|
-
buffer.byteOffset + buffer.byteLength,
|
|
92
|
-
) as ArrayBuffer;
|
|
93
|
-
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { PackageJson } from "../utils/torporPackages";
|
|
4
|
+
import { findWorkspaceRoot, workspaceDependencyDirs } from "./depCache";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Workspace packages that ship copies of their .torp components (e.g.
|
|
8
|
+
* @torpor/ui copies its src .torp files into dist/ during its build) can go
|
|
9
|
+
* stale: a site resolves the package to its dist folder, so edits to src
|
|
10
|
+
* silently keep serving the OLD components until the package is rebuilt.
|
|
11
|
+
* The symptom is confusing -- e.g. a TypeError about a renamed slot prop
|
|
12
|
+
* after a component's API changed.
|
|
13
|
+
*
|
|
14
|
+
* reportStaleTorpCopies runs at `tb --dev` startup: for every workspace
|
|
15
|
+
* dependency with both src and dist folders, it compares the .torp files and
|
|
16
|
+
* reports anything missing from dist, differing from src, or left over in
|
|
17
|
+
* dist without a src file.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reports stale dist .torp copies in the site's workspace dependencies.
|
|
22
|
+
* Never throws -- a broken check must not break dev startup.
|
|
23
|
+
*/
|
|
24
|
+
export function reportStaleTorpCopies(siteRoot: string, log?: (message: string) => void): void {
|
|
25
|
+
try {
|
|
26
|
+
const workspaceRoot = findWorkspaceRoot(siteRoot);
|
|
27
|
+
if (!workspaceRoot) return;
|
|
28
|
+
|
|
29
|
+
for (const dir of workspaceDependencyDirs(siteRoot, workspaceRoot)) {
|
|
30
|
+
const srcFolder = path.join(dir, "src");
|
|
31
|
+
const distFolder = path.join(dir, "dist");
|
|
32
|
+
// Only packages that ship dist copies of src .torp files are
|
|
33
|
+
// checked; packages without the folders (or with neither .torp
|
|
34
|
+
// set) are skipped
|
|
35
|
+
if (!existsSync(srcFolder) || !existsSync(distFolder)) continue;
|
|
36
|
+
const src = collectTorpFiles(srcFolder);
|
|
37
|
+
const dist = collectTorpFiles(distFolder);
|
|
38
|
+
if (src.size === 0 && dist.size === 0) continue;
|
|
39
|
+
|
|
40
|
+
const problems: string[] = [];
|
|
41
|
+
for (const [rel, content] of src) {
|
|
42
|
+
const distContent = dist.get(rel);
|
|
43
|
+
if (distContent === undefined) {
|
|
44
|
+
problems.push(`${rel} (not copied to dist)`);
|
|
45
|
+
} else if (distContent !== content) {
|
|
46
|
+
problems.push(`${rel} (differs from src)`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const rel of dist.keys()) {
|
|
50
|
+
if (!src.has(rel)) {
|
|
51
|
+
problems.push(`${rel} (left over in dist, no src file)`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (problems.length === 0) continue;
|
|
55
|
+
|
|
56
|
+
const shown = problems
|
|
57
|
+
.slice(0, 5)
|
|
58
|
+
.map((p) => ` ${p}`)
|
|
59
|
+
.join("\n");
|
|
60
|
+
const more = problems.length > 5 ? `\n ... and ${problems.length - 5} more` : "";
|
|
61
|
+
log?.(
|
|
62
|
+
`${packageLabel(dir, workspaceRoot)}'s dist .torp copies are stale, so the site is ` +
|
|
63
|
+
`serving OLD components:\n${shown}${more}\n` +
|
|
64
|
+
`Rebuild the package (e.g. \`pnpm build\` in ` +
|
|
65
|
+
`${path.relative(workspaceRoot, dir) || "."}) and restart \`tb --dev\`.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (process.env.TORPOR_DEBUG_CACHE) throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectTorpFiles(folder: string): Map<string, string> {
|
|
74
|
+
const files = new Map<string, string>();
|
|
75
|
+
collect(folder, folder, files);
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function collect(folder: string, root: string, files: Map<string, string>): void {
|
|
80
|
+
for (const entry of readdirSync(folder, { withFileTypes: true })) {
|
|
81
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
82
|
+
const full = path.join(folder, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
collect(full, root, files);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!entry.isFile() || !entry.name.endsWith(".torp")) continue;
|
|
88
|
+
files.set(path.relative(root, full).replaceAll("\\", "/"), readFileSync(full, "utf8"));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function packageLabel(dir: string, workspaceRoot: string): string {
|
|
93
|
+
try {
|
|
94
|
+
const pkg: PackageJson = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
95
|
+
if (pkg.name) return pkg.name;
|
|
96
|
+
} catch {
|
|
97
|
+
// Fall through to the relative path
|
|
98
|
+
}
|
|
99
|
+
return path.relative(workspaceRoot, dir) || dir;
|
|
100
|
+
}
|
package/src/run.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import run from "./run/run";
|
|
2
2
|
import runBuild from "./run/runBuild";
|
|
3
3
|
import runDev from "./run/runDev";
|
|
4
|
+
import runOpenApi from "./run/runOpenApi";
|
|
4
5
|
import runPreview from "./run/runPreview";
|
|
5
6
|
|
|
6
|
-
export { run, runDev, runBuild, runPreview };
|
|
7
|
+
export { run, runDev, runBuild, runPreview, runOpenApi };
|
package/src/schema.ts
ADDED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SerializeOptions,
|
|
1
|
+
import { type SerializeOptions, parseCookie, stringifySetCookie } from "cookie";
|
|
2
2
|
|
|
3
3
|
export default class CookieHelper {
|
|
4
4
|
request: Request;
|
|
@@ -11,11 +11,14 @@ export default class CookieHelper {
|
|
|
11
11
|
get(name: string): string | undefined {
|
|
12
12
|
// If it's been set, return the value from the map
|
|
13
13
|
if (this.cookies.has(name)) {
|
|
14
|
-
|
|
14
|
+
// The stored value is a serialized Set-Cookie header; pull the
|
|
15
|
+
// "name=value" prefix off and re-parse to get just the value
|
|
16
|
+
return parseCookie(this.cookies.get(name)!)[name];
|
|
15
17
|
}
|
|
18
|
+
|
|
16
19
|
// Otherwise, return the value from the `cookie` header
|
|
17
|
-
const header = this.request.headers.get("
|
|
18
|
-
return
|
|
20
|
+
const header = this.request.headers.get("Cookie");
|
|
21
|
+
return parseCookie(header ?? "")[name];
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
set(name: string, value: string, options?: SerializeOptions): void {
|
|
@@ -24,13 +27,20 @@ export default class CookieHelper {
|
|
|
24
27
|
options.httpOnly ??= true;
|
|
25
28
|
options.secure ??= true;
|
|
26
29
|
options.sameSite ??= "lax";
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
this.cookies.set(name, stringifySetCookie({ name, value, ...options }));
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
delete(name: string, options?: SerializeOptions): void {
|
|
31
|
-
// Set a cookie header with an immediate expiry date
|
|
35
|
+
// Set a cookie header with an empty value and an immediate expiry date.
|
|
36
|
+
// Apply the same secure defaults as `set` so a previously-set Secure
|
|
37
|
+
// cookie is actually overridden by the deletion.
|
|
32
38
|
options ??= {};
|
|
39
|
+
options.httpOnly ??= true;
|
|
40
|
+
options.secure ??= true;
|
|
41
|
+
options.sameSite ??= "lax";
|
|
33
42
|
options.expires = new Date(0);
|
|
34
|
-
|
|
43
|
+
|
|
44
|
+
this.cookies.set(name, stringifySetCookie({ name, value: "", ...options }));
|
|
35
45
|
}
|
|
36
46
|
}
|
package/src/server/Server.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import PathTrie from "../utils/pathTrie";
|
|
2
2
|
import ServerEvent from "./ServerEvent";
|
|
3
3
|
import type MiddlewareFunction from "./types/MiddlewareFunction";
|
|
4
4
|
import type ServerFunction from "./types/ServerFunction";
|
|
5
5
|
|
|
6
|
-
const noop = () => {};
|
|
7
|
-
|
|
8
6
|
/**
|
|
9
7
|
* A Fetch API Request/Response server.
|
|
10
8
|
*/
|
|
11
9
|
export default class Server {
|
|
12
10
|
// TODO: We don't use any of the routes, just add("*") in runDev/runBuild...
|
|
13
11
|
routes: RouteHandler[] = [];
|
|
12
|
+
// Match index for the routes above
|
|
13
|
+
#trie = new PathTrie<RouteHandler>();
|
|
14
14
|
// TODO: We only use the Vite middleware here
|
|
15
15
|
middleware: MiddlewareFunction[] = [];
|
|
16
16
|
|
|
@@ -25,29 +25,55 @@ export default class Server {
|
|
|
25
25
|
* @returns
|
|
26
26
|
*/
|
|
27
27
|
fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
28
|
-
|
|
28
|
+
// Use the request as-is when we were given one, so that the adapter
|
|
29
|
+
// doesn't pay for constructing it twice
|
|
30
|
+
const request = input instanceof Request && !init ? input : new Request(input, init);
|
|
29
31
|
const url = new URL(request.url);
|
|
30
32
|
const match = this.match(url.pathname);
|
|
31
|
-
let ev = new ServerEvent(request, match?.params);
|
|
33
|
+
let ev = new ServerEvent(request, match?.params, url);
|
|
32
34
|
|
|
33
35
|
if (this.middleware.length) {
|
|
34
36
|
// TODO: Get middleware that applies to this route only?
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
let entered = 0;
|
|
38
|
+
let response: Response | undefined = undefined;
|
|
39
|
+
let error: unknown = undefined;
|
|
40
|
+
try {
|
|
41
|
+
for (; entered < this.middleware.length && !response; entered++) {
|
|
42
|
+
// A middleware returns a Response to short-circuit the request
|
|
43
|
+
const result = await this.middleware[entered].enter?.(ev);
|
|
44
|
+
if (result) {
|
|
45
|
+
response = result;
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
if (!response && match) {
|
|
49
|
+
const result = await match.fn(ev);
|
|
50
|
+
if (result) {
|
|
51
|
+
response = result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
// Exits can handle the error by returning a Response; if none
|
|
56
|
+
// does, it is rethrown below
|
|
57
|
+
error = e;
|
|
58
|
+
ev.error = e;
|
|
59
|
+
} finally {
|
|
60
|
+
if (response) {
|
|
61
|
+
ev.response = response;
|
|
50
62
|
}
|
|
63
|
+
// Run exit hooks in reverse order for the middleware that were
|
|
64
|
+
// entered. An exit hook can return a Response to replace the
|
|
65
|
+
// response, or to handle ev.error
|
|
66
|
+
while (entered > 0) {
|
|
67
|
+
entered--;
|
|
68
|
+
const result = await this.middleware[entered].exit?.(ev);
|
|
69
|
+
if (result) {
|
|
70
|
+
response = result;
|
|
71
|
+
ev.response = result;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (error && !response) {
|
|
76
|
+
throw error;
|
|
51
77
|
}
|
|
52
78
|
} else if (match) {
|
|
53
79
|
ev.response = await match.fn(ev);
|
|
@@ -69,20 +95,20 @@ export default class Server {
|
|
|
69
95
|
* @returns
|
|
70
96
|
*/
|
|
71
97
|
add(path: string, fn: ServerFunction): Server {
|
|
72
|
-
|
|
98
|
+
let handler = new RouteHandler(path, fn);
|
|
99
|
+
this.routes.push(handler);
|
|
100
|
+
this.#trie.insert(path, handler);
|
|
73
101
|
this.#sortRoutes();
|
|
74
102
|
return this;
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
match(path: string): Match | undefined {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
};
|
|
85
|
-
}
|
|
106
|
+
const found = this.#trie.match(path);
|
|
107
|
+
if (found) {
|
|
108
|
+
return {
|
|
109
|
+
fn: found.value.fn,
|
|
110
|
+
params: found.params,
|
|
111
|
+
};
|
|
86
112
|
}
|
|
87
113
|
console.log(`Server path not found: ${path}`);
|
|
88
114
|
}
|
|
@@ -116,12 +142,10 @@ export default class Server {
|
|
|
116
142
|
|
|
117
143
|
class RouteHandler {
|
|
118
144
|
path: string;
|
|
119
|
-
regex: RegExp;
|
|
120
145
|
fn: ServerFunction;
|
|
121
146
|
|
|
122
147
|
constructor(path: string, fn: ServerFunction) {
|
|
123
148
|
this.path = path;
|
|
124
|
-
this.regex = pathToRegex(path);
|
|
125
149
|
this.fn = fn;
|
|
126
150
|
}
|
|
127
151
|
}
|