@torpor/build 0.1.1 → 0.1.2

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.
@@ -0,0 +1,669 @@
1
+ import {
2
+ serverError
3
+ } from "./chunk-VAGHGWM5.js";
4
+ import {
5
+ prepareTemplate
6
+ } from "./chunk-V4XTDQEV.js";
7
+ import {
8
+ flattenHeaders
9
+ } from "./chunk-5X4T3PZN.js";
10
+
11
+ // src/site/runBuild.ts
12
+ import torpor from "@torpor/unplugin/vite";
13
+ import { existsSync, promises as fs } from "node:fs";
14
+ import path2 from "node:path";
15
+ import { build, defineConfig } from "vite";
16
+
17
+ // src/site/manifest.ts
18
+ import { readFileSync } from "node:fs";
19
+ import path from "node:path";
20
+ var moduleId = "@torpor/build/manifest";
21
+ function manifest(site, server = false) {
22
+ return {
23
+ name: "torpor-manifest",
24
+ resolveId: {
25
+ order: "pre",
26
+ handler(id) {
27
+ if (id === moduleId) {
28
+ return id;
29
+ }
30
+ }
31
+ },
32
+ load(id) {
33
+ if (id === moduleId) {
34
+ return `
35
+ ${!server ? "const load = { default: { load: true } };" : ""}
36
+ export default {
37
+ routes: [
38
+ ${site.routes.map((r) => {
39
+ if (server || !/server\.(ts|js)$/.test(r.file)) {
40
+ return `{ path: "${r.path}", file: "${r.file}", type: ${r.type}, endPoint: ${server || !/server\.(ts|js)$/.test(r.file) ? `() => import(/* @vite-ignore */ "${path.join(site.root, r.file)}")` : "undefined"} }`;
41
+ } else {
42
+ const haveLoad = readFileSync(path.join(site.root, r.file)).includes("load:");
43
+ return `{ path: "${r.path}", file: "${r.file}", type: ${r.type}, endPoint: ${haveLoad ? "() => load" : "undefined"} }`;
44
+ }
45
+ }).join(",\n ")}
46
+ ],
47
+ };
48
+ `;
49
+ }
50
+ }
51
+ };
52
+ }
53
+
54
+ // src/site/runBuild.ts
55
+ async function runBuild(site) {
56
+ const distFolder = path2.resolve(site.root, "dist");
57
+ if (existsSync(distFolder)) {
58
+ await fs.rm(distFolder, { recursive: true });
59
+ }
60
+ const clientFolder = path2.join(distFolder, "client");
61
+ const serverFolder = path2.join(distFolder, "server");
62
+ if (site.adapter.prebuild) {
63
+ await site.adapter.prebuild(site);
64
+ }
65
+ let siteHtml = path2.resolve(site.root, "src/site.html");
66
+ const siteFolder = path2.resolve(site.root, "./node_modules/@torpor/build/src/site/");
67
+ let clientScript = path2.join(siteFolder, "clientEntry.ts");
68
+ let serverScript = path2.join(siteFolder, "serverEntry.ts");
69
+ await build(
70
+ defineConfig({
71
+ plugins: [
72
+ manifest(site),
73
+ // @ts-ignore
74
+ torpor(),
75
+ ...site.plugins
76
+ ],
77
+ build: {
78
+ outDir: clientFolder,
79
+ rollupOptions: {
80
+ input: [
81
+ siteHtml,
82
+ clientScript,
83
+ ...site.routes.filter((r) => !/server\.(ts|js)$/.test(r.file)).map((r) => path2.resolve(site.root, r.file))
84
+ ]
85
+ },
86
+ ssrManifest: true
87
+ }
88
+ })
89
+ );
90
+ await build(
91
+ defineConfig({
92
+ plugins: [
93
+ manifest(site, true),
94
+ // @ts-ignore
95
+ torpor(),
96
+ ...site.plugins
97
+ ],
98
+ build: {
99
+ outDir: serverFolder,
100
+ rollupOptions: {
101
+ input: [serverScript, ...site.routes.map((r) => path2.resolve(site.root, r.file))]
102
+ },
103
+ ssr: serverScript
104
+ }
105
+ })
106
+ );
107
+ siteHtml = path2.join(clientFolder, "site.html");
108
+ await fs.rename(path2.join(clientFolder, "src", "site.html"), siteHtml);
109
+ await fs.rm(path2.join(clientFolder, "src"), { recursive: true });
110
+ if (site.adapter.postbuild) {
111
+ await site.adapter.postbuild(site);
112
+ }
113
+ }
114
+
115
+ // src/site/runDev.ts
116
+ import torpor2 from "@torpor/unplugin/vite";
117
+ import { configDotenv } from "dotenv";
118
+ import { promises as fs2 } from "node:fs";
119
+ import path3 from "node:path";
120
+ import { fileURLToPath } from "node:url";
121
+ import { createServer as createViteServer } from "vite";
122
+
123
+ // src/adapters/node/nodeMessageToNodeResponse.ts
124
+ import {
125
+ ServerResponse
126
+ } from "node:http";
127
+ import { PassThrough, Readable } from "node:stream";
128
+ function nodeMessageToNodeResponse(incomingMessage) {
129
+ const res = new ServerResponse(incomingMessage);
130
+ const passThrough = new PassThrough();
131
+ let handled = false;
132
+ const onReadable = (cb) => {
133
+ const handleReadable = () => {
134
+ if (handled) return;
135
+ handled = true;
136
+ cb({
137
+ readable: Readable.from(passThrough),
138
+ headers: res.getHeaders(),
139
+ status: res.statusCode
140
+ });
141
+ };
142
+ passThrough.once("readable", handleReadable);
143
+ passThrough.once("end", handleReadable);
144
+ };
145
+ passThrough.once("finish", () => {
146
+ res.emit("finish");
147
+ });
148
+ passThrough.once("close", () => {
149
+ res.destroy();
150
+ res.emit("close");
151
+ });
152
+ passThrough.on("drain", () => {
153
+ res.emit("drain");
154
+ });
155
+ res.write = passThrough.write.bind(passThrough);
156
+ res.end = passThrough.end.bind(passThrough);
157
+ res.writeHead = function writeHead(statusCode, statusMessage, headers) {
158
+ res.statusCode = statusCode;
159
+ if (typeof statusMessage === "object") {
160
+ headers = statusMessage;
161
+ statusMessage = void 0;
162
+ }
163
+ if (headers) {
164
+ for (const [key, value] of Object.entries(headers)) {
165
+ if (value !== void 0) {
166
+ res.setHeader(key, value);
167
+ }
168
+ }
169
+ }
170
+ return res;
171
+ };
172
+ return {
173
+ res,
174
+ onReadable
175
+ };
176
+ }
177
+
178
+ // src/adapters/node/readableToBuffer.ts
179
+ function readableToBuffer(req) {
180
+ return new Promise((resolve, reject) => {
181
+ const chunks = [];
182
+ req.on("data", (chunk) => {
183
+ chunks.push(chunk);
184
+ });
185
+ req.on("end", () => {
186
+ resolve(Buffer.concat(chunks));
187
+ });
188
+ req.on("error", (err) => {
189
+ reject(err);
190
+ });
191
+ });
192
+ }
193
+
194
+ // src/adapters/node/requestToNodeMessage.ts
195
+ import { Readable as Readable2 } from "node:stream";
196
+ var DUMMY_BASE_URL = "http://localhost";
197
+ function requestToNodeMessage(request) {
198
+ const parsedUrl = new URL(request.url, DUMMY_BASE_URL);
199
+ const pathAndQuery = (parsedUrl.pathname || "") + (parsedUrl.search || "");
200
+ const body = (
201
+ /*request.body ? Readable.fromWeb(request.body as any) :*/
202
+ Readable2.from([])
203
+ );
204
+ return Object.assign(body, {
205
+ url: pathAndQuery,
206
+ method: request.method,
207
+ headers: Object.fromEntries(request.headers)
208
+ });
209
+ }
210
+
211
+ // src/adapters/node/createMiddlewareHandler.ts
212
+ function createMiddlewareHandler(handler) {
213
+ return async (ev, next) => {
214
+ const req = requestToNodeMessage(ev.request);
215
+ const { res, onReadable } = nodeMessageToNodeResponse(req);
216
+ return new Promise(async (resolve, reject) => {
217
+ onReadable(async ({ readable, headers, status }) => {
218
+ const responseBody = statusCodesWithoutBody.includes(status) ? null : (
219
+ //(Readable.toWeb(readable) as ReadableStream);
220
+ // HACK: Chunked bodies are not being
221
+ // read correctly somewhere...
222
+ await readableToBuffer(readable)
223
+ );
224
+ ev.response = new Response(responseBody, {
225
+ status,
226
+ headers: flattenHeaders(headers)
227
+ });
228
+ resolve();
229
+ });
230
+ const cnext = async (error) => {
231
+ if (error) {
232
+ reject(error instanceof Error ? error : new Error(String(error)));
233
+ } else {
234
+ await next();
235
+ resolve();
236
+ }
237
+ };
238
+ try {
239
+ const handled = await handler(req, res, cnext);
240
+ if (handled === false) {
241
+ res.destroy();
242
+ resolve();
243
+ }
244
+ } catch (e) {
245
+ cnext(e);
246
+ }
247
+ });
248
+ };
249
+ }
250
+ var statusCodesWithoutBody = [
251
+ 100,
252
+ // Continue
253
+ 101,
254
+ // Switching Protocols
255
+ 102,
256
+ // Processing (WebDAV)
257
+ 103,
258
+ // Early Hints
259
+ 204,
260
+ // No Content
261
+ 205,
262
+ // Reset Content
263
+ 304
264
+ // Not Modified
265
+ ];
266
+
267
+ // src/site/pathToRegex.ts
268
+ function pathToRegex(path6) {
269
+ const pattern = path6.split("/").map((p) => {
270
+ return p.replace(/\[([^\/]+?)\]/, "(?<$1>[^\\/]+?)");
271
+ }).join("\\/").replaceAll("*", ".*");
272
+ return new RegExp(`^${pattern}$`);
273
+ }
274
+
275
+ // src/server/CookieHelper.ts
276
+ import { parse, serialize } from "cookie";
277
+ var CookieHelper = class {
278
+ request;
279
+ cookies = /* @__PURE__ */ new Map();
280
+ constructor(request) {
281
+ this.request = request;
282
+ }
283
+ get(name) {
284
+ if (this.cookies.has(name)) {
285
+ return this.cookies.get(name);
286
+ }
287
+ const header = this.request.headers.get("cookie");
288
+ return parse(header ?? "")[name];
289
+ }
290
+ set(name, value, options) {
291
+ options ??= {};
292
+ options.httpOnly ??= true;
293
+ options.secure ??= true;
294
+ options.sameSite ??= "lax";
295
+ this.cookies.set(name, serialize(name, value, options));
296
+ }
297
+ delete(name, options) {
298
+ options ??= {};
299
+ options.expires = /* @__PURE__ */ new Date(0);
300
+ this.cookies.set(name, serialize(name, "", options));
301
+ }
302
+ };
303
+
304
+ // src/server/HeaderHelper.ts
305
+ var HeaderHelper = class {
306
+ request;
307
+ headers = /* @__PURE__ */ new Map();
308
+ constructor(request) {
309
+ this.request = request;
310
+ }
311
+ get(name) {
312
+ if (this.headers.has(name)) {
313
+ return this.headers.get(name);
314
+ }
315
+ return this.request.headers.get(name) ?? void 0;
316
+ }
317
+ set(name, value) {
318
+ this.headers.set(name, value);
319
+ }
320
+ delete(name) {
321
+ this.headers.delete(name);
322
+ }
323
+ };
324
+
325
+ // src/server/ServerEvent.ts
326
+ var ServerEvent = class {
327
+ request;
328
+ response;
329
+ // TODO: Should we cast types??
330
+ params;
331
+ cookies;
332
+ headers;
333
+ constructor(request, params) {
334
+ this.request = request;
335
+ this.params = params;
336
+ this.cookies = new CookieHelper(request);
337
+ this.headers = new HeaderHelper(request);
338
+ }
339
+ addHeaders() {
340
+ if (!this.response) {
341
+ throw new Error("Response not created yet");
342
+ }
343
+ for (let cookie of this.cookies.cookies.values()) {
344
+ this.response.headers.append("set-cookie", cookie);
345
+ }
346
+ for (let header of this.headers.headers.entries()) {
347
+ this.response.headers.append(header[0], header[1]);
348
+ }
349
+ }
350
+ };
351
+
352
+ // src/server/Server.ts
353
+ var noop = () => {
354
+ };
355
+ var Server = class {
356
+ // TODO: We don't use any of the routes, just add("*") in runDev/runBuild...
357
+ routes = [];
358
+ // TODO: We only use the Vite middleware here
359
+ middleware = [];
360
+ constructor() {
361
+ this.routes = [];
362
+ this.middleware = [];
363
+ }
364
+ /**
365
+ * The standard Fetch API method that should work in any runtime (including
366
+ * Node with polyfills).
367
+ * @returns
368
+ */
369
+ fetch = async (input, init) => {
370
+ const request = new Request(input, init);
371
+ const url = new URL(request.url);
372
+ const match = this.match(url.pathname);
373
+ let ev = new ServerEvent(request, match?.params);
374
+ if (this.middleware.length) {
375
+ let buildNext2 = function(middleware, i) {
376
+ if (ev.response) {
377
+ return noop;
378
+ }
379
+ if (i < middleware.length - 1) {
380
+ let next = middleware[i + 1];
381
+ return async () => next(ev, buildNext2(middleware, i + 1));
382
+ } else {
383
+ return match ? async () => {
384
+ ev.response = await match.fn(ev);
385
+ } : noop;
386
+ }
387
+ };
388
+ var buildNext = buildNext2;
389
+ await this.middleware[0](ev, buildNext2(this.middleware, 0));
390
+ } else if (match) {
391
+ ev.response = await match.fn(ev);
392
+ } else {
393
+ return new Response(null, { status: 404 });
394
+ }
395
+ ev.response ??= new Response(null, { status: 200 });
396
+ ev.addHeaders();
397
+ return ev.response;
398
+ };
399
+ /**
400
+ * Adds a server function at a route path.
401
+ * @param path The route path pattern.
402
+ * @param fn The function to call when the pattern is matched.
403
+ * @returns
404
+ */
405
+ add(path6, fn) {
406
+ this.routes.push(new RouteHandler(path6, fn));
407
+ return this;
408
+ }
409
+ match(path6) {
410
+ for (let handler of this.routes) {
411
+ let match = path6.match(handler.regex);
412
+ if (match) {
413
+ return {
414
+ fn: handler.fn,
415
+ params: match.groups
416
+ };
417
+ }
418
+ }
419
+ console.log(`Server path not found: ${path6}`);
420
+ }
421
+ // TODO:
422
+ //use(path: string, ...fn: MiddlewareFunction[])
423
+ use(...fn) {
424
+ this.middleware = this.middleware.concat(fn);
425
+ return this;
426
+ }
427
+ };
428
+ var RouteHandler = class {
429
+ path;
430
+ regex;
431
+ fn;
432
+ constructor(path6, fn) {
433
+ this.path = path6;
434
+ this.regex = pathToRegex(path6);
435
+ this.fn = fn;
436
+ }
437
+ };
438
+
439
+ // src/site/runDev.ts
440
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
441
+ async function runDev(site) {
442
+ const server = new Server();
443
+ const vite = await createViteServer({
444
+ server: { middlewareMode: true },
445
+ appType: "custom",
446
+ plugins: [
447
+ manifest(site, true),
448
+ // @ts-ignore not sure...
449
+ torpor2(),
450
+ ...site.plugins
451
+ ]
452
+ });
453
+ const templateFile = path3.resolve(site.root, "src/site.html");
454
+ let template = await fs2.readFile(templateFile, "utf-8");
455
+ template = await vite.transformIndexHtml("", template);
456
+ const clientScript = path3.resolve(__dirname, "../../src/site/clientEntry.ts");
457
+ const serverScript = path3.resolve(__dirname, "../../src/site/serverEntry.ts");
458
+ template = prepareTemplate(template, clientScript);
459
+ server.use(createMiddlewareHandler(vite.middlewares));
460
+ server.add("*", async (ev) => {
461
+ try {
462
+ const { load } = await vite.ssrLoadModule(serverScript);
463
+ return await load(ev, template);
464
+ } catch (e) {
465
+ vite.ssrFixStacktrace(e);
466
+ return serverError(e);
467
+ }
468
+ });
469
+ configDotenv();
470
+ process.env.PROTOCOL ??= "http:";
471
+ process.env.HOST ??= "localhost";
472
+ process.env.PORT ??= "7059";
473
+ site.adapter.serve(server, site);
474
+ }
475
+
476
+ // src/site/runPreview.ts
477
+ import { configDotenv as configDotenv2 } from "dotenv";
478
+ import { existsSync as existsSync2, promises as fs3 } from "node:fs";
479
+ import path4 from "node:path";
480
+
481
+ // src/server/contentType.ts
482
+ function contentType(extension) {
483
+ return types[extension] || "application/octet-stream";
484
+ }
485
+ var types = {
486
+ ".aac": "audio/aac",
487
+ ".abw": "application/x-abiword",
488
+ ".apng": "image/apng",
489
+ ".arc": "application/x-freearc",
490
+ ".avif": "image/avif",
491
+ ".avi": "video/x-msvideo",
492
+ ".azw": "application/vnd.amazon.ebook",
493
+ ".bin": "application/octet-stream",
494
+ ".bmp": "image/bmp",
495
+ ".bz": "application/x-bzip",
496
+ ".bz2": "application/x-bzip2",
497
+ ".cda": "application/x-cdf",
498
+ ".csh": "application/x-csh",
499
+ ".css": "text/css",
500
+ ".csv": "text/csv",
501
+ ".doc": "application/msword",
502
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
503
+ ".eot": "application/vnd.ms-fontobject",
504
+ ".epub": "application/epub+zip",
505
+ ".gz": "application/gzip",
506
+ ".gif": "image/gif",
507
+ ".htm": "text/html",
508
+ ".html": "text/html",
509
+ ".ico": "image/vnd.microsoft.icon",
510
+ ".ics": "text/calendar",
511
+ ".jar": "application/java-archive",
512
+ ".jpeg": "image/jpeg",
513
+ ".jpg": "image/jpeg",
514
+ ".js": "text/javascript",
515
+ ".json": "application/json",
516
+ ".jsonld": "application/ld+json",
517
+ ".mid": "audio/midi",
518
+ ".midi": "audio/midi",
519
+ ".mjs": "text/javascript",
520
+ ".mp3": "audio/mpeg",
521
+ ".mp4": "video/mp4",
522
+ ".mpeg": "video/mpeg",
523
+ ".mpkg": "application/vnd.apple.installer+xml",
524
+ ".odp": "application/vnd.oasis.opendocument.presentation",
525
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
526
+ ".odt": "application/vnd.oasis.opendocument.text",
527
+ ".oga": "audio/ogg",
528
+ ".ogv": "video/ogg",
529
+ ".ogx": "application/ogg",
530
+ ".opus": "audio/ogg",
531
+ ".otf": "font/otf",
532
+ ".png": "image/png",
533
+ ".pdf": "application/pdf",
534
+ ".php": "application/x-httpd-php",
535
+ ".ppt": "application/vnd.ms-powerpoint",
536
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
537
+ ".rar": "application/vnd.rar",
538
+ ".rtf": "application/rtf",
539
+ ".sh": "application/x-sh",
540
+ ".svg": "image/svg+xml",
541
+ ".tar": "application/x-tar",
542
+ ".tif": "image/tiff",
543
+ ".tiff": "image/tiff",
544
+ ".ts": "video/mp2t",
545
+ ".ttf": "font/ttf",
546
+ ".txt": "text/plain",
547
+ ".vsd": "application/vnd.visio",
548
+ ".wav": "audio/wav",
549
+ ".weba": "audio/webm",
550
+ ".webm": "video/webm",
551
+ ".webp": "image/webp",
552
+ ".woff": "font/woff",
553
+ ".woff2": "font/woff2",
554
+ ".xhtml": "application/xhtml+xml",
555
+ ".xls": "application/vnd.ms-excel",
556
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
557
+ ".xml": "application/xml",
558
+ ".xul": "application/vnd.mozilla.xul+xml",
559
+ ".zip": "application/zip",
560
+ ".3gp": "video/3gpp",
561
+ ".3g2": "video/3gpp2",
562
+ ".7z": "application/x-7z-compressed"
563
+ };
564
+
565
+ // src/site/runPreview.ts
566
+ async function runPreview(site) {
567
+ await runBuild(site);
568
+ const distFolder = path4.resolve(site.root, "dist");
569
+ const clientFolder = path4.join(distFolder, "client");
570
+ const serverFolder = path4.join(distFolder, "server");
571
+ let clientScript = (await fs3.readdir(path4.join(clientFolder, "assets"))).find(
572
+ (f) => f.startsWith("clientEntry-")
573
+ );
574
+ if (!clientScript) {
575
+ throw new Error("clientEntry.js not found");
576
+ }
577
+ clientScript = `/assets/${clientScript}`;
578
+ let siteHtml = path4.join(clientFolder, "site.html");
579
+ let template = await fs3.readFile(siteHtml, "utf-8");
580
+ template = prepareTemplate(template, clientScript);
581
+ const server = new Server();
582
+ server.add("/assets/*", async (ev) => {
583
+ try {
584
+ const url = new URL(ev.request.url);
585
+ const file = path4.join(site.root, "dist", "client", url.pathname);
586
+ if (existsSync2(file)) {
587
+ return new Response(await fs3.readFile(file), {
588
+ status: 200,
589
+ headers: {
590
+ "Content-Type": contentType(path4.extname(file))
591
+ }
592
+ });
593
+ }
594
+ } catch (e) {
595
+ return serverError(e);
596
+ }
597
+ });
598
+ const serverScript = path4.resolve(serverFolder, "serverEntry.js");
599
+ server.add("*", async (ev) => {
600
+ try {
601
+ const { load } = await import(serverScript);
602
+ return await load(ev, template);
603
+ } catch (e) {
604
+ return serverError(e);
605
+ }
606
+ });
607
+ configDotenv2();
608
+ process.env.PROTOCOL ??= "http:";
609
+ process.env.HOST ??= "localhost";
610
+ process.env.PORT ??= "7059";
611
+ site.adapter.serve(server, site);
612
+ }
613
+
614
+ // src/site/run.ts
615
+ import fs4 from "node:fs";
616
+ import path5 from "node:path";
617
+ import { transformWithEsbuild } from "vite";
618
+ async function run(folder, mode) {
619
+ let configFile = "";
620
+ let deleteConfigFile = false;
621
+ const jsConfigFile = path5.join(folder, "site.config.js");
622
+ const tsConfigFile = path5.join(folder, "site.config.ts");
623
+ if (fs4.existsSync(jsConfigFile)) {
624
+ configFile = jsConfigFile;
625
+ } else if (fs4.existsSync(tsConfigFile)) {
626
+ configFile = path5.join(folder, "site.config.temp.js");
627
+ let source = (await transformWithEsbuild(fs4.readFileSync(tsConfigFile, "utf-8"), tsConfigFile, {
628
+ loader: "ts"
629
+ })).code;
630
+ fs4.writeFileSync(configFile, source);
631
+ deleteConfigFile = true;
632
+ } else {
633
+ throw new Error("site.config file not found");
634
+ }
635
+ const site = (await import(configFile)).default;
636
+ if (!site || !site.root || !site.routes) {
637
+ throw new Error("Invalid site in config file");
638
+ }
639
+ if (deleteConfigFile) {
640
+ fs4.rmSync(configFile);
641
+ }
642
+ switch (mode) {
643
+ case "dev": {
644
+ await runDev(site);
645
+ break;
646
+ }
647
+ case "build": {
648
+ await runBuild(site);
649
+ break;
650
+ }
651
+ case "preview": {
652
+ await runPreview(site);
653
+ break;
654
+ }
655
+ default: {
656
+ console.log("No mode passed, running in dev");
657
+ await runDev(site);
658
+ break;
659
+ }
660
+ }
661
+ }
662
+
663
+ export {
664
+ runBuild,
665
+ runDev,
666
+ runPreview,
667
+ run
668
+ };
669
+ //# sourceMappingURL=chunk-ENB7RXFX.js.map