zuby 1.0.34 → 1.0.35
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/commands/build.js +7 -2
- package/commands/dev.js +8 -29
- package/config.js +3 -1
- package/package.json +1 -1
- package/plugins/contextPlugin/index.d.ts +3 -3
- package/plugins/contextPlugin/index.js +12 -15
- package/server/index.js +89 -64
- package/server/types.d.ts +7 -0
- package/server/zubyDevRenderer.d.ts +10 -0
- package/server/zubyDevRenderer.js +37 -0
- package/server/zubyDevServer.d.ts +11 -0
- package/server/zubyDevServer.js +117 -0
- package/server/zubyRenderer.d.ts +2 -1
- package/server/zubyRenderer.js +19 -7
- package/server/zubyServer.d.ts +12 -11
- package/server/zubyServer.js +50 -42
package/commands/build.js
CHANGED
|
@@ -16,7 +16,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
16
16
|
const __dirname = dirname(__filename);
|
|
17
17
|
export default async function build(options) {
|
|
18
18
|
const zubyInternalConfig = await getZubyInternalConfig(options.configFile);
|
|
19
|
-
const { vite: viteConfig, customLogger: logger, outDir, srcDir, publicDir, configFilePath, } = zubyInternalConfig;
|
|
19
|
+
const { vite: viteConfig, customLogger: logger, outDir, srcDir, cacheDir, publicDir, configFilePath, } = zubyInternalConfig;
|
|
20
20
|
process.env.NODE_ENV = MODES.production;
|
|
21
21
|
logger?.info(getTitle(chalk.gray(`building for production...`)));
|
|
22
22
|
// Clean build directory
|
|
@@ -36,6 +36,7 @@ export default async function build(options) {
|
|
|
36
36
|
build: {
|
|
37
37
|
...viteConfig?.build,
|
|
38
38
|
outDir: normalizePath(join(outDir, 'client')),
|
|
39
|
+
ssrManifest: 'chunks-manifest.json',
|
|
39
40
|
rollupOptions: {
|
|
40
41
|
...viteConfig?.build?.rollupOptions,
|
|
41
42
|
input: {
|
|
@@ -43,7 +44,9 @@ export default async function build(options) {
|
|
|
43
44
|
},
|
|
44
45
|
},
|
|
45
46
|
},
|
|
47
|
+
cacheDir: normalizePath(join(cacheDir, 'client')),
|
|
46
48
|
mode: MODES.production,
|
|
49
|
+
appType: 'custom',
|
|
47
50
|
});
|
|
48
51
|
// Server build
|
|
49
52
|
logger?.info(`${chalk.bgYellow.bold.whiteBright(` Step 2/4 `)} ${chalk.gray(`building server...`)}`);
|
|
@@ -55,7 +58,7 @@ export default async function build(options) {
|
|
|
55
58
|
...viteConfig?.build,
|
|
56
59
|
outDir: normalizePath(join(outDir, 'server')),
|
|
57
60
|
ssr: normalizePath(entryFile),
|
|
58
|
-
ssrManifest: '
|
|
61
|
+
ssrManifest: 'chunks-manifest.json',
|
|
59
62
|
target: 'node18',
|
|
60
63
|
},
|
|
61
64
|
optimizeDeps: {
|
|
@@ -65,7 +68,9 @@ export default async function build(options) {
|
|
|
65
68
|
force: true,
|
|
66
69
|
disabled: false,
|
|
67
70
|
},
|
|
71
|
+
cacheDir: normalizePath(join(cacheDir, 'server')),
|
|
68
72
|
mode: MODES.production,
|
|
73
|
+
appType: 'custom',
|
|
69
74
|
});
|
|
70
75
|
// Add serialized zuby config to build directory
|
|
71
76
|
writeFileSync(normalizePath(join(outDir, 'zuby.config.json')), JSON.stringify(zubyInternalConfig, null, 2));
|
package/commands/dev.js
CHANGED
|
@@ -1,39 +1,18 @@
|
|
|
1
|
-
import { MODES } from '../types.js';
|
|
2
1
|
import { getZubyInternalConfig } from '../config.js';
|
|
3
|
-
import { createServer } from 'vite';
|
|
4
|
-
import { normalizePath } from '../utils/pathUtils.js';
|
|
5
|
-
import { join } from 'path';
|
|
6
2
|
import { performance } from 'node:perf_hooks';
|
|
7
3
|
import { getTitle } from '../branding.js';
|
|
8
4
|
import chalk from 'chalk';
|
|
5
|
+
import ZubyDevServer from '../server/zubyDevServer.js';
|
|
9
6
|
export default async function dev(options) {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const host = options.host || viteConfig?.server?.host;
|
|
13
|
-
const mode = MODES.development;
|
|
14
|
-
const serverConfig = {
|
|
15
|
-
...viteConfig,
|
|
16
|
-
server: {
|
|
17
|
-
...viteConfig?.server,
|
|
18
|
-
port,
|
|
19
|
-
host,
|
|
20
|
-
},
|
|
21
|
-
build: {
|
|
22
|
-
outDir: normalizePath(join(outDir, 'client')),
|
|
23
|
-
},
|
|
24
|
-
mode,
|
|
25
|
-
};
|
|
7
|
+
const zubyConfig = await getZubyInternalConfig(options.configFile);
|
|
8
|
+
const { customLogger: logger } = zubyConfig;
|
|
26
9
|
const startTime = performance.now();
|
|
27
|
-
const
|
|
28
|
-
|
|
10
|
+
const { host = '127.0.0.1', port = 3000 } = zubyConfig.server;
|
|
11
|
+
const zubyDevServer = new ZubyDevServer(zubyConfig);
|
|
12
|
+
await zubyDevServer.listen();
|
|
29
13
|
const readyTime = performance.now();
|
|
30
|
-
if (!server.httpServer) {
|
|
31
|
-
throw new Error('HTTP server not available');
|
|
32
|
-
}
|
|
33
|
-
const bindAddress = server.httpServer.address();
|
|
34
|
-
const bindPort = typeof bindAddress === 'string' ? undefined : bindAddress?.port;
|
|
35
14
|
logger?.info(` ${getTitle(chalk.gray(`started in ${Math.round(readyTime - startTime)}ms`))}\r\n`);
|
|
36
|
-
logger?.info(` ┃ Mode
|
|
37
|
-
logger?.info(` ┃ Local http://${host}:${
|
|
15
|
+
logger?.info(` ┃ Mode development`);
|
|
16
|
+
logger?.info(` ┃ Local http://${host}:${port}/`);
|
|
38
17
|
logger?.info(` ┃ Network ${chalk.gray('use --host and --port to expose')}\r\n`);
|
|
39
18
|
}
|
package/config.js
CHANGED
|
@@ -77,7 +77,8 @@ export const mergeDefaultConfig = async (config) => {
|
|
|
77
77
|
// Set default values
|
|
78
78
|
config.srcDir = config.srcDir ?? './';
|
|
79
79
|
config.publicDir = config.publicDir ?? 'public';
|
|
80
|
-
config.outDir = config.outDir ?? '
|
|
80
|
+
config.outDir = config.outDir ?? '.zuby';
|
|
81
|
+
config.cacheDir = config.cacheDir ?? '.zuby-cache';
|
|
81
82
|
config.output = config.output ?? 'static';
|
|
82
83
|
config.prerenderPaths = config.prerenderPaths ?? [];
|
|
83
84
|
// Default server config
|
|
@@ -104,6 +105,7 @@ export const mergeDefaultConfig = async (config) => {
|
|
|
104
105
|
config.vite.root = config.vite.root ?? config.srcDir;
|
|
105
106
|
config.vite.base = config.vite.base ?? '/';
|
|
106
107
|
config.vite.build.outDir = config.vite.build.outDir ?? config.outDir;
|
|
108
|
+
config.vite.cacheDir = config.vite.cacheDir ?? config.cacheDir;
|
|
107
109
|
config.vite.logLevel = config.vite.logLevel ?? config.logLevel;
|
|
108
110
|
config.vite.customLogger = config.vite.customLogger ?? config.customLogger;
|
|
109
111
|
config.vite.server = config.vite.server ?? config.server;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PluginOption } from 'vite';
|
|
2
2
|
import { Template } from '../../templates/types.js';
|
|
3
3
|
export default function index(): PluginOption;
|
|
4
|
-
export declare function generateCompileTimeContextCode(): Promise<string>;
|
|
5
|
-
export declare function generateTemplatesCode(): Promise<string>;
|
|
4
|
+
export declare function generateCompileTimeContextCode(ssr: boolean): Promise<string>;
|
|
5
|
+
export declare function generateTemplatesCode(ssr: boolean): Promise<string>;
|
|
6
6
|
export declare function generateTemplateCode(template: Template): Promise<string>;
|
|
7
|
-
export declare function generateRenderCode(): Promise<string>;
|
|
7
|
+
export declare function generateRenderCode(ssr: boolean): Promise<string>;
|
|
@@ -11,28 +11,29 @@ export default function index() {
|
|
|
11
11
|
async configResolved(config) {
|
|
12
12
|
viteConfig = config;
|
|
13
13
|
},
|
|
14
|
-
async transform(code, id) {
|
|
14
|
+
async transform(code, id, options) {
|
|
15
|
+
const { ssr = false } = options || {};
|
|
15
16
|
if (!id.includes('entry') || !/\.(js|ts|jsx|tsx|mjs|cjs)(\?.+)?$/.test(id)) {
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
|
-
const contextCode = await generateCompileTimeContextCode();
|
|
19
|
+
const contextCode = await generateCompileTimeContextCode(ssr);
|
|
19
20
|
return contextCode + code;
|
|
20
21
|
},
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
|
-
export async function generateCompileTimeContextCode() {
|
|
24
|
+
export async function generateCompileTimeContextCode(ssr) {
|
|
24
25
|
const { site } = await getZubyInternalConfig();
|
|
25
26
|
const { version } = await getZubyPackageConfig();
|
|
26
27
|
return `globalThis.ZubyRawContext = {
|
|
27
28
|
...(globalThis.ZubyRawContext || {}),
|
|
28
|
-
templates: ${await generateTemplatesCode()},
|
|
29
|
-
render: ${await generateRenderCode()},
|
|
29
|
+
templates: ${await generateTemplatesCode(ssr)},
|
|
30
|
+
render: ${await generateRenderCode(ssr)},
|
|
30
31
|
site: '${site || ''}',
|
|
31
32
|
generator: 'Zuby.js ${version}',
|
|
32
33
|
version: '${version}',
|
|
33
34
|
};`;
|
|
34
35
|
}
|
|
35
|
-
export async function generateTemplatesCode() {
|
|
36
|
+
export async function generateTemplatesCode(ssr) {
|
|
36
37
|
const templates = await getTemplates();
|
|
37
38
|
const pages = await getPages(templates);
|
|
38
39
|
const apps = await getApps(templates);
|
|
@@ -43,15 +44,11 @@ export async function generateTemplatesCode() {
|
|
|
43
44
|
const pagesCode = await Promise.all(pages.map(generateTemplateCode) || []);
|
|
44
45
|
const appsCode = await Promise.all(apps.map(generateTemplateCode) || []);
|
|
45
46
|
const errorsCode = await Promise.all(errors.map(generateTemplateCode) || []);
|
|
46
|
-
const layoutsCode =
|
|
47
|
-
|
|
48
|
-
: [];
|
|
49
|
-
const innerLayoutsCode = viteConfig?.build.ssr
|
|
47
|
+
const layoutsCode = ssr ? await Promise.all(layouts.map(generateTemplateCode) || []) : [];
|
|
48
|
+
const innerLayoutsCode = ssr
|
|
50
49
|
? await Promise.all(innerLayouts.map(generateTemplateCode) || [])
|
|
51
50
|
: [];
|
|
52
|
-
const handlersCode =
|
|
53
|
-
? await Promise.all(handlers.map(generateTemplateCode) || [])
|
|
54
|
-
: [];
|
|
51
|
+
const handlersCode = ssr ? await Promise.all(handlers.map(generateTemplateCode) || []) : [];
|
|
55
52
|
return `{
|
|
56
53
|
pages: [${pagesCode.join(',')}],
|
|
57
54
|
apps: [${appsCode.join(',')}],
|
|
@@ -72,9 +69,9 @@ export async function generateTemplateCode(template) {
|
|
|
72
69
|
component: () => import("${template.filename}"),
|
|
73
70
|
}`;
|
|
74
71
|
}
|
|
75
|
-
export async function generateRenderCode() {
|
|
72
|
+
export async function generateRenderCode(ssr) {
|
|
76
73
|
const { jsx } = await getZubyInternalConfig();
|
|
77
|
-
if (!
|
|
74
|
+
if (!ssr)
|
|
78
75
|
return '{}';
|
|
79
76
|
return `await import("${jsx.renderFile}")`;
|
|
80
77
|
}
|
package/server/index.js
CHANGED
|
@@ -751,7 +751,7 @@ var getTitle = (title) => {
|
|
|
751
751
|
|
|
752
752
|
// src/server/index.ts
|
|
753
753
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
754
|
-
import { dirname as dirname2 } from "path";
|
|
754
|
+
import { dirname as dirname2, resolve as resolve4 } from "path";
|
|
755
755
|
|
|
756
756
|
// src/logger/index.ts
|
|
757
757
|
import readline from "node:readline";
|
|
@@ -864,9 +864,7 @@ function createLogger(level = "info", options = {}) {
|
|
|
864
864
|
|
|
865
865
|
// src/server/zubyServer.ts
|
|
866
866
|
import { createServer as createHttpsServer } from "node:https";
|
|
867
|
-
import {
|
|
868
|
-
createServer as createHttpServer
|
|
869
|
-
} from "node:http";
|
|
867
|
+
import { createServer as createHttpServer } from "node:http";
|
|
870
868
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
871
869
|
|
|
872
870
|
// src/utils/pathUtils.ts
|
|
@@ -877,7 +875,7 @@ function normalizePath(path2) {
|
|
|
877
875
|
|
|
878
876
|
// src/server/zubyServer.ts
|
|
879
877
|
import { resolve as resolve3 } from "path";
|
|
880
|
-
import { createReadStream, existsSync, statSync } from "fs";
|
|
878
|
+
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
881
879
|
|
|
882
880
|
// src/server/mimeTypes.ts
|
|
883
881
|
var mimeTypes = Object.freeze({
|
|
@@ -2063,9 +2061,6 @@ var mimeTypes = Object.freeze({
|
|
|
2063
2061
|
".zmm": "application/vnd.handheld-entertainment+xml"
|
|
2064
2062
|
});
|
|
2065
2063
|
|
|
2066
|
-
// src/server/zubyServer.ts
|
|
2067
|
-
import { performance as performance2 } from "node:perf_hooks";
|
|
2068
|
-
|
|
2069
2064
|
// src/context/index.ts
|
|
2070
2065
|
var ZubyContext = class {
|
|
2071
2066
|
constructor(rawContext) {
|
|
@@ -5472,10 +5467,10 @@ var Minipass = class extends EventEmitter {
|
|
|
5472
5467
|
* Return a void Promise that resolves once the stream ends.
|
|
5473
5468
|
*/
|
|
5474
5469
|
async promise() {
|
|
5475
|
-
return new Promise((
|
|
5470
|
+
return new Promise((resolve5, reject) => {
|
|
5476
5471
|
this.on(DESTROYED, () => reject(new Error("stream destroyed")));
|
|
5477
5472
|
this.on("error", (er) => reject(er));
|
|
5478
|
-
this.on("end", () =>
|
|
5473
|
+
this.on("end", () => resolve5());
|
|
5479
5474
|
});
|
|
5480
5475
|
}
|
|
5481
5476
|
/**
|
|
@@ -5499,7 +5494,7 @@ var Minipass = class extends EventEmitter {
|
|
|
5499
5494
|
return Promise.resolve({ done: false, value: res });
|
|
5500
5495
|
if (this[EOF])
|
|
5501
5496
|
return stop();
|
|
5502
|
-
let
|
|
5497
|
+
let resolve5;
|
|
5503
5498
|
let reject;
|
|
5504
5499
|
const onerr = (er) => {
|
|
5505
5500
|
this.off("data", ondata);
|
|
@@ -5513,19 +5508,19 @@ var Minipass = class extends EventEmitter {
|
|
|
5513
5508
|
this.off("end", onend);
|
|
5514
5509
|
this.off(DESTROYED, ondestroy);
|
|
5515
5510
|
this.pause();
|
|
5516
|
-
|
|
5511
|
+
resolve5({ value, done: !!this[EOF] });
|
|
5517
5512
|
};
|
|
5518
5513
|
const onend = () => {
|
|
5519
5514
|
this.off("error", onerr);
|
|
5520
5515
|
this.off("data", ondata);
|
|
5521
5516
|
this.off(DESTROYED, ondestroy);
|
|
5522
5517
|
stop();
|
|
5523
|
-
|
|
5518
|
+
resolve5({ done: true, value: void 0 });
|
|
5524
5519
|
};
|
|
5525
5520
|
const ondestroy = () => onerr(new Error("stream destroyed"));
|
|
5526
5521
|
return new Promise((res2, rej) => {
|
|
5527
5522
|
reject = rej;
|
|
5528
|
-
|
|
5523
|
+
resolve5 = res2;
|
|
5529
5524
|
this.once(DESTROYED, ondestroy);
|
|
5530
5525
|
this.once("error", onerr);
|
|
5531
5526
|
this.once("end", onend);
|
|
@@ -6476,9 +6471,9 @@ var PathBase = class {
|
|
|
6476
6471
|
if (this.#asyncReaddirInFlight) {
|
|
6477
6472
|
await this.#asyncReaddirInFlight;
|
|
6478
6473
|
} else {
|
|
6479
|
-
let
|
|
6474
|
+
let resolve5 = () => {
|
|
6480
6475
|
};
|
|
6481
|
-
this.#asyncReaddirInFlight = new Promise((res) =>
|
|
6476
|
+
this.#asyncReaddirInFlight = new Promise((res) => resolve5 = res);
|
|
6482
6477
|
try {
|
|
6483
6478
|
for (const e of await this.#fs.promises.readdir(fullpath, {
|
|
6484
6479
|
withFileTypes: true
|
|
@@ -6491,7 +6486,7 @@ var PathBase = class {
|
|
|
6491
6486
|
children.provisional = 0;
|
|
6492
6487
|
}
|
|
6493
6488
|
this.#asyncReaddirInFlight = void 0;
|
|
6494
|
-
|
|
6489
|
+
resolve5();
|
|
6495
6490
|
}
|
|
6496
6491
|
return children.slice(0, children.provisional);
|
|
6497
6492
|
}
|
|
@@ -8352,7 +8347,7 @@ var glob = Object.assign(glob_, {
|
|
|
8352
8347
|
glob.glob = glob;
|
|
8353
8348
|
|
|
8354
8349
|
// src/server/zubyRenderer.ts
|
|
8355
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
8350
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
8356
8351
|
|
|
8357
8352
|
// src/templates/types.ts
|
|
8358
8353
|
var BASE_TEMPLATES = {
|
|
@@ -8397,15 +8392,25 @@ var ZubyRenderer = class {
|
|
|
8397
8392
|
constructor(outDir) {
|
|
8398
8393
|
this.outDir = outDir;
|
|
8399
8394
|
}
|
|
8395
|
+
async reload() {
|
|
8396
|
+
await this.init();
|
|
8397
|
+
}
|
|
8400
8398
|
async init() {
|
|
8401
8399
|
const entryPath = normalizePath(resolve2(this.outDir, "server", "entry.js"));
|
|
8402
8400
|
const entryModule = await import(`file:///${entryPath}`);
|
|
8403
8401
|
this.entry = entryModule.default || entryModule;
|
|
8404
8402
|
this.entryClientJs = (await glob(`${this.outDir}/client/chunks/entry-*.js`)).pop();
|
|
8405
8403
|
this.entryClientCss = (await glob(`${this.outDir}/client/chunks/entry-*.css`)).pop();
|
|
8406
|
-
this.
|
|
8407
|
-
|
|
8408
|
-
|
|
8404
|
+
if (this.entryClientJs) {
|
|
8405
|
+
this.entryClientJs = `/chunks/${basename(this.entryClientJs || "")}`;
|
|
8406
|
+
}
|
|
8407
|
+
if (this.entryClientCss) {
|
|
8408
|
+
this.entryClientCss = `/chunks/${basename(this.entryClientCss || "")}`;
|
|
8409
|
+
}
|
|
8410
|
+
const manifestPath = resolve2(this.outDir, "client", "chunks-manifest.json");
|
|
8411
|
+
if (existsSync(manifestPath)) {
|
|
8412
|
+
this.chunksManifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
|
|
8413
|
+
}
|
|
8409
8414
|
this.zubyContext = getContext();
|
|
8410
8415
|
this.renderToString = this.zubyContext.renderToString;
|
|
8411
8416
|
this.renderToStream = this.zubyContext.renderToStream;
|
|
@@ -8463,16 +8468,16 @@ var ZubyRenderer = class {
|
|
|
8463
8468
|
});
|
|
8464
8469
|
let html = await this.renderToString?.(layoutChildren) || "";
|
|
8465
8470
|
const staticImports = [
|
|
8466
|
-
...this.
|
|
8467
|
-
...this.
|
|
8471
|
+
...this.chunksManifest?.[app?.filename || ""] || [],
|
|
8472
|
+
...this.chunksManifest?.[page?.filename || ""] || []
|
|
8468
8473
|
];
|
|
8469
8474
|
const cssImports = staticImports.filter((imp) => imp.endsWith(".css"));
|
|
8470
|
-
const jsImports =
|
|
8475
|
+
const jsImports = staticImports.filter((imp) => imp.endsWith(".js"));
|
|
8471
8476
|
if (this.entryClientCss) {
|
|
8472
|
-
cssImports.unshift(
|
|
8477
|
+
cssImports.unshift(this.entryClientCss);
|
|
8473
8478
|
}
|
|
8474
8479
|
if (this.entryClientJs) {
|
|
8475
|
-
jsImports.unshift(
|
|
8480
|
+
jsImports.unshift(this.entryClientJs);
|
|
8476
8481
|
}
|
|
8477
8482
|
html = "<!DOCTYPE html>" + html;
|
|
8478
8483
|
cssImports.forEach((imp) => {
|
|
@@ -8514,7 +8519,13 @@ var ZubyServer = class {
|
|
|
8514
8519
|
this.options = options;
|
|
8515
8520
|
this.options.responseHeaders = this.options.responseHeaders || {};
|
|
8516
8521
|
this.logger = options.logger || createLogger("info");
|
|
8517
|
-
this.renderer = new ZubyRenderer(options.outDir);
|
|
8522
|
+
this.renderer = options.renderer || new ZubyRenderer(options.outDir);
|
|
8523
|
+
this.middlewares = [
|
|
8524
|
+
this.defaultHeadersMiddleware.bind(this),
|
|
8525
|
+
this.trailingSlashMiddleware.bind(this),
|
|
8526
|
+
this.clientDirMiddleware.bind(this),
|
|
8527
|
+
this.serverDirMiddleware.bind(this)
|
|
8528
|
+
];
|
|
8518
8529
|
const { https, httpsKeyFile, httpsCertFile } = options;
|
|
8519
8530
|
if (https && httpsKeyFile && httpsCertFile) {
|
|
8520
8531
|
this.server = createHttpsServer(
|
|
@@ -8529,37 +8540,35 @@ var ZubyServer = class {
|
|
|
8529
8540
|
}
|
|
8530
8541
|
}
|
|
8531
8542
|
async handle(nodeReq, nodeRes) {
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8543
|
+
let index = 0;
|
|
8544
|
+
const next = async () => {
|
|
8545
|
+
if (index < this.middlewares.length) {
|
|
8546
|
+
const middleware = this.middlewares[index];
|
|
8547
|
+
index++;
|
|
8548
|
+
await middleware(nodeReq, nodeRes, next);
|
|
8549
|
+
}
|
|
8550
|
+
};
|
|
8551
|
+
await next();
|
|
8536
8552
|
}
|
|
8537
|
-
async
|
|
8553
|
+
async clientDirMiddleware(req, res, next) {
|
|
8538
8554
|
const { outDir } = this.options;
|
|
8539
8555
|
const parsedUrl = new URL(req.url || "", "http://localhost:3000");
|
|
8540
8556
|
const normalizedPath = normalizePath(parsedUrl.pathname);
|
|
8541
8557
|
let file = normalizePath(
|
|
8542
8558
|
resolve3(outDir, "client", normalizedPath.substring(1))
|
|
8543
8559
|
);
|
|
8544
|
-
const exists =
|
|
8560
|
+
const exists = existsSync2(file);
|
|
8545
8561
|
const isDirectory = exists && statSync(file).isDirectory();
|
|
8546
|
-
if (isDirectory && normalizedPath.length > 0 && !normalizedPath.endsWith("/")) {
|
|
8547
|
-
const location = `${parsedUrl.pathname}/${parsedUrl.search || ""}`;
|
|
8548
|
-
res.statusCode = 302;
|
|
8549
|
-
res.setHeader("Location", location);
|
|
8550
|
-
res.end();
|
|
8551
|
-
return true;
|
|
8552
|
-
}
|
|
8553
8562
|
if (!exists)
|
|
8554
|
-
return
|
|
8563
|
+
return next();
|
|
8555
8564
|
if (isDirectory) {
|
|
8556
8565
|
file = [
|
|
8557
8566
|
normalizePath(resolve3(file, "index.html")),
|
|
8558
8567
|
normalizePath(resolve3(file, "index.json"))
|
|
8559
|
-
].find(
|
|
8568
|
+
].find(existsSync2);
|
|
8560
8569
|
}
|
|
8561
8570
|
if (!file)
|
|
8562
|
-
return
|
|
8571
|
+
return next();
|
|
8563
8572
|
const fileStats = statSync(file);
|
|
8564
8573
|
const extension = file.split(".").pop() || "";
|
|
8565
8574
|
const contentType = mimeTypes[`.${extension}`] || "application/octet-stream";
|
|
@@ -8587,47 +8596,52 @@ var ZubyServer = class {
|
|
|
8587
8596
|
headers["Content-Length"] = (end - start + 1).toString();
|
|
8588
8597
|
headers["Accept-Ranges"] = "bytes";
|
|
8589
8598
|
}
|
|
8590
|
-
res.writeHead(statusCode,
|
|
8599
|
+
res.writeHead(statusCode, headers);
|
|
8591
8600
|
createReadStream(file, streamOptions).pipe(res).on("finish", () => {
|
|
8592
8601
|
res.end();
|
|
8593
8602
|
});
|
|
8594
|
-
return true;
|
|
8595
8603
|
}
|
|
8596
|
-
async
|
|
8604
|
+
async serverDirMiddleware(nodeReq, nodeRes) {
|
|
8597
8605
|
const req = await this.toRequest(nodeReq);
|
|
8598
8606
|
const res = await this.renderer.render(req);
|
|
8599
8607
|
return this.toNodeResponse(res, nodeRes);
|
|
8600
8608
|
}
|
|
8601
|
-
async
|
|
8602
|
-
const serverPromise = new Promise((
|
|
8609
|
+
async listen() {
|
|
8610
|
+
const serverPromise = new Promise((resolve5, reject) => {
|
|
8603
8611
|
this.server.listen(this.options.port, this.options.host, 511, () => {
|
|
8604
8612
|
if (!this.server.listening)
|
|
8605
8613
|
return reject(new Error(`Server could not start`));
|
|
8606
|
-
|
|
8614
|
+
resolve5(void 0);
|
|
8607
8615
|
});
|
|
8608
8616
|
});
|
|
8609
8617
|
const rendererPromise = this.renderer.init();
|
|
8610
8618
|
await Promise.all([serverPromise, rendererPromise]);
|
|
8611
8619
|
}
|
|
8612
|
-
async
|
|
8620
|
+
async close() {
|
|
8613
8621
|
this.server.closeAllConnections();
|
|
8614
|
-
return new Promise((
|
|
8622
|
+
return new Promise((resolve5, reject) => {
|
|
8615
8623
|
this.server.close((error) => {
|
|
8616
8624
|
if (error)
|
|
8617
8625
|
return reject(error);
|
|
8618
|
-
|
|
8626
|
+
resolve5(void 0);
|
|
8619
8627
|
});
|
|
8620
8628
|
});
|
|
8621
8629
|
}
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8630
|
+
defaultHeadersMiddleware(_req, res, next) {
|
|
8631
|
+
res.setHeader("X-Powered-By", "Zuby.js");
|
|
8632
|
+
next();
|
|
8633
|
+
}
|
|
8634
|
+
trailingSlashMiddleware(req, res, next) {
|
|
8635
|
+
const [path2, query] = req.url?.split("?") || [];
|
|
8636
|
+
if (path2.endsWith("/") || path2.match(/[.](.+)$/)) {
|
|
8637
|
+
return next();
|
|
8638
|
+
}
|
|
8639
|
+
res.statusCode = 302;
|
|
8640
|
+
res.setHeader("Location", `${path2}/${query ? `?${query}` : ""}`);
|
|
8641
|
+
res.end();
|
|
8628
8642
|
}
|
|
8629
8643
|
async toRequest(nodeReq) {
|
|
8630
|
-
return new Promise((
|
|
8644
|
+
return new Promise((resolve5, reject) => {
|
|
8631
8645
|
try {
|
|
8632
8646
|
let buffer = [];
|
|
8633
8647
|
nodeReq.on("data", (chunk) => buffer.push(chunk)).on("end", () => {
|
|
@@ -8639,7 +8653,7 @@ var ZubyServer = class {
|
|
|
8639
8653
|
for (const key in nodeReq.headers) {
|
|
8640
8654
|
headers.append(key, nodeReq.headers[key]);
|
|
8641
8655
|
}
|
|
8642
|
-
|
|
8656
|
+
resolve5(
|
|
8643
8657
|
new Request(url, {
|
|
8644
8658
|
method: nodeReq.method,
|
|
8645
8659
|
body: nodeReq.method === "POST" ? body : null,
|
|
@@ -8655,27 +8669,38 @@ var ZubyServer = class {
|
|
|
8655
8669
|
async toNodeResponse(res, nodeRes) {
|
|
8656
8670
|
const headers = {};
|
|
8657
8671
|
res.headers.forEach((value, key) => headers[key] = value);
|
|
8658
|
-
nodeRes.writeHead(res.status,
|
|
8672
|
+
nodeRes.writeHead(res.status, headers);
|
|
8659
8673
|
nodeRes.end(await res.text());
|
|
8660
8674
|
}
|
|
8675
|
+
use(middleware) {
|
|
8676
|
+
this.middlewares.push(middleware);
|
|
8677
|
+
}
|
|
8678
|
+
useBefore(middleware) {
|
|
8679
|
+
this.middlewares.unshift(middleware);
|
|
8680
|
+
}
|
|
8681
|
+
async reload() {
|
|
8682
|
+
await this.renderer.reload();
|
|
8683
|
+
}
|
|
8661
8684
|
};
|
|
8662
8685
|
|
|
8663
8686
|
// src/server/index.ts
|
|
8687
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
8664
8688
|
var __filename = fileURLToPath4(import.meta.url);
|
|
8665
8689
|
var __dirname = dirname2(__filename);
|
|
8666
8690
|
var logger = createLogger("info");
|
|
8691
|
+
var config = JSON.parse(readFileSync4(resolve4(__dirname, "zuby.config.json"), "utf-8"));
|
|
8667
8692
|
var mode = process.env["NODE_ENV"] || "production";
|
|
8668
8693
|
var envPort = process.env["PORT"] ? Number(process.env["PORT"]) : void 0;
|
|
8669
8694
|
var envHost = process.env["HOST"];
|
|
8670
|
-
var port = envPort || 3e3;
|
|
8671
|
-
var host = envHost || "127.0.0.1";
|
|
8695
|
+
var port = envPort || config?.server?.port || 3e3;
|
|
8696
|
+
var host = envHost || config?.server?.host || "127.0.0.1";
|
|
8672
8697
|
var zubyServer = new ZubyServer({
|
|
8673
8698
|
port,
|
|
8674
8699
|
host,
|
|
8675
8700
|
outDir: __dirname
|
|
8676
8701
|
});
|
|
8677
8702
|
if (mode === "production") {
|
|
8678
|
-
await zubyServer.
|
|
8703
|
+
await zubyServer.listen();
|
|
8679
8704
|
logger?.info(` ${getTitle("")}\r
|
|
8680
8705
|
`);
|
|
8681
8706
|
logger?.info(` \u2503 Mode ${"production"}`);
|
package/server/types.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
1
2
|
import { ZubyLogger } from '../logger/types.js';
|
|
3
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
+
import ZubyRenderer from './zubyRenderer.js';
|
|
5
|
+
export type NodeRequest = IncomingMessage;
|
|
6
|
+
export type NodeResponse = ServerResponse;
|
|
2
7
|
export type ZubyHeaders = Record<string, string | string[] | undefined>;
|
|
3
8
|
export interface ZubyServerOptions {
|
|
4
9
|
port: number;
|
|
@@ -9,4 +14,6 @@ export interface ZubyServerOptions {
|
|
|
9
14
|
httpsKeyFile?: string;
|
|
10
15
|
logger?: ZubyLogger;
|
|
11
16
|
responseHeaders?: ZubyHeaders;
|
|
17
|
+
renderer?: ZubyRenderer;
|
|
12
18
|
}
|
|
19
|
+
export type ZubyMiddleware = (req: NodeRequest, res: NodeResponse, next: () => void | Promise<void>) => void | Promise<void>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ZubyRenderer from './zubyRenderer.js';
|
|
2
|
+
import { ZubyInternalConfig } from '../types.js';
|
|
3
|
+
import { ViteDevServer } from 'vite';
|
|
4
|
+
export default class ZubyDevRenderer extends ZubyRenderer {
|
|
5
|
+
protected zubyInternalConfig: ZubyInternalConfig;
|
|
6
|
+
protected viteServerDevServer: ViteDevServer;
|
|
7
|
+
constructor(zubyConfig: ZubyInternalConfig, viteServerDevServer: ViteDevServer);
|
|
8
|
+
init(): Promise<void>;
|
|
9
|
+
render(req: Request): Promise<Response>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { normalizePath } from '../utils/pathUtils.js';
|
|
2
|
+
import { relative } from 'path';
|
|
3
|
+
import { getContext } from '../context/index.js';
|
|
4
|
+
import ZubyRenderer from './zubyRenderer.js';
|
|
5
|
+
import { getEntryFile } from '../commands/build.js';
|
|
6
|
+
export default class ZubyDevRenderer extends ZubyRenderer {
|
|
7
|
+
constructor(zubyConfig, viteServerDevServer) {
|
|
8
|
+
super(zubyConfig.outDir);
|
|
9
|
+
this.zubyInternalConfig = zubyConfig;
|
|
10
|
+
this.viteServerDevServer = viteServerDevServer;
|
|
11
|
+
}
|
|
12
|
+
async init() {
|
|
13
|
+
const entryPath = this.zubyInternalConfig.jsx.entryTemplateFile;
|
|
14
|
+
const entryModule = await this.viteServerDevServer.ssrLoadModule(entryPath);
|
|
15
|
+
this.entry = entryModule.default || entryModule;
|
|
16
|
+
this.zubyContext = getContext();
|
|
17
|
+
this.renderToString = this.zubyContext.renderToString;
|
|
18
|
+
this.renderToStream = this.zubyContext.renderToStream;
|
|
19
|
+
// Load the entry file from the project directory
|
|
20
|
+
// or jsxProvider
|
|
21
|
+
const entryFile = await getEntryFile(this.zubyInternalConfig);
|
|
22
|
+
this.entryClientJs = '/' + normalizePath(relative(process.cwd(), entryFile));
|
|
23
|
+
}
|
|
24
|
+
async render(req) {
|
|
25
|
+
const originalRes = await super.render(req);
|
|
26
|
+
// Do not transform non-html responses
|
|
27
|
+
if (!originalRes.headers.get('content-type')?.includes('text/html'))
|
|
28
|
+
return originalRes;
|
|
29
|
+
const body = await originalRes.text();
|
|
30
|
+
const bodyTransformed = await this.viteServerDevServer.transformIndexHtml(req.url, body);
|
|
31
|
+
return new Response(bodyTransformed, {
|
|
32
|
+
headers: originalRes.headers,
|
|
33
|
+
status: originalRes.status,
|
|
34
|
+
statusText: originalRes.statusText,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ZubyServer from './zubyServer.js';
|
|
2
|
+
import { ZubyInternalConfig } from '../types.js';
|
|
3
|
+
import { ViteDevServer } from 'vite';
|
|
4
|
+
export default class ZubyDevServer extends ZubyServer {
|
|
5
|
+
protected zubyInternalConfig: ZubyInternalConfig;
|
|
6
|
+
protected viteClientDevServer?: ViteDevServer;
|
|
7
|
+
protected viteServerDevServer?: ViteDevServer;
|
|
8
|
+
constructor(zubyConfig: ZubyInternalConfig);
|
|
9
|
+
listen(): Promise<void>;
|
|
10
|
+
reload(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import ZubyServer from './zubyServer.js';
|
|
2
|
+
import { MODES } from '../types.js';
|
|
3
|
+
import ZubyDevRenderer from './zubyDevRenderer.js';
|
|
4
|
+
import { createServer } from 'vite';
|
|
5
|
+
import { normalizePath } from '../utils/pathUtils.js';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { getEntryFile } from '../commands/build.js';
|
|
8
|
+
import { rmSync, watch } from 'fs';
|
|
9
|
+
export default class ZubyDevServer extends ZubyServer {
|
|
10
|
+
constructor(zubyConfig) {
|
|
11
|
+
super({
|
|
12
|
+
port: zubyConfig.server.port || 3000,
|
|
13
|
+
host: zubyConfig.server.host || '127.0.0.1',
|
|
14
|
+
outDir: zubyConfig.outDir,
|
|
15
|
+
});
|
|
16
|
+
this.zubyInternalConfig = zubyConfig;
|
|
17
|
+
this.middlewares = [
|
|
18
|
+
this.defaultHeadersMiddleware.bind(this),
|
|
19
|
+
this.trailingSlashMiddleware.bind(this),
|
|
20
|
+
this.serverDirMiddleware.bind(this),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
async listen() {
|
|
24
|
+
// Clear cache directory
|
|
25
|
+
rmSync(this.zubyInternalConfig.cacheDir, {
|
|
26
|
+
recursive: true,
|
|
27
|
+
force: true,
|
|
28
|
+
});
|
|
29
|
+
// Load the entry file from the project directory
|
|
30
|
+
// or jsxProvider
|
|
31
|
+
const entryFile = await getEntryFile(this.zubyInternalConfig);
|
|
32
|
+
this.viteClientDevServer = await createServer({
|
|
33
|
+
configFile: false,
|
|
34
|
+
...this.zubyInternalConfig.vite,
|
|
35
|
+
server: {
|
|
36
|
+
...this.zubyInternalConfig.vite?.server,
|
|
37
|
+
middlewareMode: true,
|
|
38
|
+
port: this.options.port,
|
|
39
|
+
host: this.options.host,
|
|
40
|
+
hmr: {
|
|
41
|
+
port: this.options.port + 1,
|
|
42
|
+
host: this.options.host,
|
|
43
|
+
protocol: 'ws',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
build: {
|
|
47
|
+
...this.zubyInternalConfig.vite?.build,
|
|
48
|
+
outDir: normalizePath(join(this.zubyInternalConfig.outDir, 'client')),
|
|
49
|
+
ssr: false,
|
|
50
|
+
ssrManifest: 'chunks-manifest.json',
|
|
51
|
+
rollupOptions: {
|
|
52
|
+
...this.zubyInternalConfig.vite?.build?.rollupOptions,
|
|
53
|
+
input: {
|
|
54
|
+
entry: normalizePath(entryFile),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
watch: {
|
|
58
|
+
include: `${this.zubyInternalConfig.srcDir}/**/*`,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
cacheDir: normalizePath(join(this.zubyInternalConfig.cacheDir, 'client')),
|
|
62
|
+
mode: MODES.development,
|
|
63
|
+
appType: 'custom',
|
|
64
|
+
});
|
|
65
|
+
this.viteServerDevServer = await createServer({
|
|
66
|
+
...this.zubyInternalConfig.vite,
|
|
67
|
+
server: {
|
|
68
|
+
...this.zubyInternalConfig.vite?.server,
|
|
69
|
+
middlewareMode: true,
|
|
70
|
+
port: this.options.port,
|
|
71
|
+
host: this.options.host,
|
|
72
|
+
hmr: {
|
|
73
|
+
port: this.options.port + 2,
|
|
74
|
+
host: this.options.host,
|
|
75
|
+
protocol: 'ws',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
configFile: false,
|
|
79
|
+
publicDir: false,
|
|
80
|
+
build: {
|
|
81
|
+
...this.zubyInternalConfig.vite?.build,
|
|
82
|
+
outDir: normalizePath(join(this.zubyInternalConfig.outDir, 'server')),
|
|
83
|
+
ssr: normalizePath(entryFile),
|
|
84
|
+
ssrManifest: 'chunks-manifest.json',
|
|
85
|
+
ssrEmitAssets: true,
|
|
86
|
+
target: 'node18',
|
|
87
|
+
watch: {
|
|
88
|
+
include: `${this.zubyInternalConfig.srcDir}/**/*`,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
optimizeDeps: {
|
|
92
|
+
...this.zubyInternalConfig.vite.optimizeDeps,
|
|
93
|
+
entries: ['**/*'],
|
|
94
|
+
include: ['**/*'],
|
|
95
|
+
force: true,
|
|
96
|
+
disabled: false,
|
|
97
|
+
},
|
|
98
|
+
cacheDir: normalizePath(join(this.zubyInternalConfig.cacheDir, 'server')),
|
|
99
|
+
mode: MODES.development,
|
|
100
|
+
appType: 'custom',
|
|
101
|
+
});
|
|
102
|
+
this.useBefore(this.viteClientDevServer.middlewares);
|
|
103
|
+
this.useBefore(this.viteServerDevServer.middlewares);
|
|
104
|
+
this.renderer = new ZubyDevRenderer(this.zubyInternalConfig, this.viteServerDevServer);
|
|
105
|
+
watch(join(this.zubyInternalConfig.srcDir, 'pages'), { recursive: true }, (eventType, fileName) => {
|
|
106
|
+
if (eventType === 'rename')
|
|
107
|
+
this.reload.bind(this)();
|
|
108
|
+
});
|
|
109
|
+
await this.renderer.init();
|
|
110
|
+
await super.listen();
|
|
111
|
+
}
|
|
112
|
+
async reload() {
|
|
113
|
+
this.viteClientDevServer?.moduleGraph.invalidateAll();
|
|
114
|
+
this.viteServerDevServer?.moduleGraph.invalidateAll();
|
|
115
|
+
await this.renderer.reload();
|
|
116
|
+
}
|
|
117
|
+
}
|
package/server/zubyRenderer.d.ts
CHANGED
|
@@ -10,10 +10,11 @@ export default class ZubyRenderer {
|
|
|
10
10
|
protected entry?: EntryType;
|
|
11
11
|
protected entryClientJs?: string;
|
|
12
12
|
protected entryClientCss?: string;
|
|
13
|
-
protected
|
|
13
|
+
protected chunksManifest?: {
|
|
14
14
|
[key: string]: string[];
|
|
15
15
|
};
|
|
16
16
|
constructor(outDir: string);
|
|
17
|
+
reload(): Promise<void>;
|
|
17
18
|
init(): Promise<void>;
|
|
18
19
|
executeHandler(pageContext: ZubyPageContext): Promise<undefined | Response>;
|
|
19
20
|
executeProps(pageContext: ZubyPageContext): Promise<undefined | Response>;
|
package/server/zubyRenderer.js
CHANGED
|
@@ -3,19 +3,31 @@ import { ZubyPageContext } from '../pageContext/index.js';
|
|
|
3
3
|
import { normalizePath } from '../utils/pathUtils.js';
|
|
4
4
|
import { basename, resolve } from 'path';
|
|
5
5
|
import { glob } from 'glob';
|
|
6
|
-
import { readFileSync } from 'fs';
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
7
|
import { findMatchingTemplate } from '../templates/pathUtils.js';
|
|
8
8
|
export default class ZubyRenderer {
|
|
9
9
|
constructor(outDir) {
|
|
10
10
|
this.outDir = outDir;
|
|
11
11
|
}
|
|
12
|
+
async reload() {
|
|
13
|
+
await this.init();
|
|
14
|
+
}
|
|
12
15
|
async init() {
|
|
13
16
|
const entryPath = normalizePath(resolve(this.outDir, 'server', 'entry.js'));
|
|
14
17
|
const entryModule = await import(`file:///${entryPath}`);
|
|
15
18
|
this.entry = entryModule.default || entryModule;
|
|
16
19
|
this.entryClientJs = (await glob(`${this.outDir}/client/chunks/entry-*.js`)).pop();
|
|
17
20
|
this.entryClientCss = (await glob(`${this.outDir}/client/chunks/entry-*.css`)).pop();
|
|
18
|
-
|
|
21
|
+
if (this.entryClientJs) {
|
|
22
|
+
this.entryClientJs = `/chunks/${basename(this.entryClientJs || '')}`;
|
|
23
|
+
}
|
|
24
|
+
if (this.entryClientCss) {
|
|
25
|
+
this.entryClientCss = `/chunks/${basename(this.entryClientCss || '')}`;
|
|
26
|
+
}
|
|
27
|
+
const manifestPath = resolve(this.outDir, 'client', 'chunks-manifest.json');
|
|
28
|
+
if (existsSync(manifestPath)) {
|
|
29
|
+
this.chunksManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
30
|
+
}
|
|
19
31
|
this.zubyContext = getContext();
|
|
20
32
|
this.renderToString = this.zubyContext.renderToString;
|
|
21
33
|
this.renderToStream = this.zubyContext.renderToStream;
|
|
@@ -82,18 +94,18 @@ export default class ZubyRenderer {
|
|
|
82
94
|
// Render the layout
|
|
83
95
|
let html = (await this.renderToString?.(layoutChildren)) || '';
|
|
84
96
|
const staticImports = [
|
|
85
|
-
...(this.
|
|
86
|
-
...(this.
|
|
97
|
+
...(this.chunksManifest?.[app?.filename || ''] || []),
|
|
98
|
+
...(this.chunksManifest?.[page?.filename || ''] || []),
|
|
87
99
|
];
|
|
88
100
|
const cssImports = staticImports.filter((imp) => imp.endsWith('.css'));
|
|
89
|
-
const jsImports =
|
|
101
|
+
const jsImports = staticImports.filter((imp) => imp.endsWith('.js'));
|
|
90
102
|
// Entry css
|
|
91
103
|
if (this.entryClientCss) {
|
|
92
|
-
cssImports.unshift(
|
|
104
|
+
cssImports.unshift(this.entryClientCss);
|
|
93
105
|
}
|
|
94
106
|
// Entry js
|
|
95
107
|
if (this.entryClientJs) {
|
|
96
|
-
jsImports.unshift(
|
|
108
|
+
jsImports.unshift(this.entryClientJs);
|
|
97
109
|
}
|
|
98
110
|
// Insert document type
|
|
99
111
|
html = '<!DOCTYPE html>' + html;
|
package/server/zubyServer.d.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
3
|
import { Server as HttpsServer } from 'node:https';
|
|
4
|
-
import { Server as HttpServer,
|
|
5
|
-
import {
|
|
4
|
+
import { Server as HttpServer, ServerResponse } from 'node:http';
|
|
5
|
+
import { ZubyServerOptions, NodeResponse, NodeRequest, ZubyMiddleware } from './types.js';
|
|
6
6
|
import { ZubyLogger } from '../logger/types.js';
|
|
7
7
|
import ZubyRenderer from './zubyRenderer.js';
|
|
8
|
-
export type NodeRequest = IncomingMessage;
|
|
9
|
-
export type NodeResponse = ServerResponse;
|
|
10
8
|
export default class ZubyServer {
|
|
11
9
|
protected options: ZubyServerOptions;
|
|
12
10
|
protected server: HttpServer | HttpsServer;
|
|
13
11
|
protected logger: ZubyLogger;
|
|
14
12
|
protected renderer: ZubyRenderer;
|
|
13
|
+
protected middlewares: ZubyMiddleware[];
|
|
15
14
|
constructor(options: ZubyServerOptions);
|
|
16
15
|
handle(nodeReq: NodeRequest, nodeRes: NodeResponse): Promise<void>;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
16
|
+
clientDirMiddleware(req: NodeRequest, res: NodeResponse, next: () => void): Promise<void | NodeResponse>;
|
|
17
|
+
serverDirMiddleware(nodeReq: NodeRequest, nodeRes: NodeResponse): Promise<void>;
|
|
18
|
+
listen(): Promise<void>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
defaultHeadersMiddleware(_req: NodeRequest, res: NodeResponse, next: () => void): void;
|
|
21
|
+
trailingSlashMiddleware(req: NodeRequest, res: NodeResponse, next: () => void): void;
|
|
24
22
|
toRequest(nodeReq: NodeRequest): Promise<Request>;
|
|
25
23
|
toNodeResponse(res: Response, nodeRes: ServerResponse): Promise<void>;
|
|
24
|
+
use(middleware: ZubyMiddleware): void;
|
|
25
|
+
useBefore(middleware: ZubyMiddleware): void;
|
|
26
|
+
reload(): Promise<void>;
|
|
26
27
|
}
|
package/server/zubyServer.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { createServer as createHttpsServer } from 'node:https';
|
|
2
|
-
import { createServer as createHttpServer
|
|
2
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { normalizePath } from '../utils/pathUtils.js';
|
|
5
5
|
import { resolve } from 'path';
|
|
6
6
|
import { createReadStream, existsSync, statSync } from 'fs';
|
|
7
7
|
import { mimeTypes } from './mimeTypes.js';
|
|
8
8
|
import { createLogger } from '../logger/index.js';
|
|
9
|
-
import { performance } from 'node:perf_hooks';
|
|
10
9
|
import ZubyRenderer from './zubyRenderer.js';
|
|
11
10
|
export default class ZubyServer {
|
|
12
11
|
constructor(options) {
|
|
13
12
|
this.options = options;
|
|
14
13
|
this.options.responseHeaders = this.options.responseHeaders || {};
|
|
15
14
|
this.logger = options.logger || createLogger('info');
|
|
16
|
-
this.renderer = new ZubyRenderer(options.outDir);
|
|
15
|
+
this.renderer = options.renderer || new ZubyRenderer(options.outDir);
|
|
16
|
+
this.middlewares = [
|
|
17
|
+
this.defaultHeadersMiddleware.bind(this),
|
|
18
|
+
this.trailingSlashMiddleware.bind(this),
|
|
19
|
+
this.clientDirMiddleware.bind(this),
|
|
20
|
+
this.serverDirMiddleware.bind(this),
|
|
21
|
+
];
|
|
17
22
|
const { https, httpsKeyFile, httpsCertFile } = options;
|
|
18
23
|
if (https && httpsKeyFile && httpsCertFile) {
|
|
19
24
|
// Create the HTTPS server
|
|
@@ -28,41 +33,27 @@ export default class ZubyServer {
|
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
async handle(nodeReq, nodeRes) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// this.logger.info(`Request: ${nodeReq.url} ${nodeRes.statusCode} ${endTime - startTime}ms`);
|
|
41
|
-
// } catch (error: any) {
|
|
42
|
-
// this.logger.error(error?.message);
|
|
43
|
-
// nodeRes.statusCode = 500;
|
|
44
|
-
// nodeRes.end('Internal server error');
|
|
45
|
-
// }
|
|
36
|
+
let index = 0;
|
|
37
|
+
const next = async () => {
|
|
38
|
+
if (index < this.middlewares.length) {
|
|
39
|
+
const middleware = this.middlewares[index];
|
|
40
|
+
index++;
|
|
41
|
+
await middleware(nodeReq, nodeRes, next);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
await next();
|
|
46
45
|
}
|
|
47
|
-
async
|
|
46
|
+
async clientDirMiddleware(req, res, next) {
|
|
48
47
|
const { outDir } = this.options;
|
|
49
48
|
const parsedUrl = new URL(req.url || '', 'http://localhost:3000');
|
|
50
49
|
const normalizedPath = normalizePath(parsedUrl.pathname);
|
|
51
50
|
let file = normalizePath(resolve(outDir, 'client', normalizedPath.substring(1)));
|
|
52
51
|
const exists = existsSync(file);
|
|
53
52
|
const isDirectory = exists && statSync(file).isDirectory();
|
|
54
|
-
if (isDirectory && normalizedPath.length > 0 && !normalizedPath.endsWith('/')) {
|
|
55
|
-
// Redirect to path with trailing slash for consistency
|
|
56
|
-
const location = `${parsedUrl.pathname}/${parsedUrl.search || ''}`;
|
|
57
|
-
res.statusCode = 302;
|
|
58
|
-
res.setHeader('Location', location);
|
|
59
|
-
res.end();
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
53
|
// Do nothing if the file does not exist.
|
|
63
54
|
// ssrHandler will handle the request.
|
|
64
55
|
if (!exists)
|
|
65
|
-
return
|
|
56
|
+
return next();
|
|
66
57
|
// If the requested file is a directory, try to load the index file
|
|
67
58
|
if (isDirectory) {
|
|
68
59
|
file = [
|
|
@@ -73,7 +64,7 @@ export default class ZubyServer {
|
|
|
73
64
|
// Server will handle the request
|
|
74
65
|
// if we could not find the file
|
|
75
66
|
if (!file)
|
|
76
|
-
return
|
|
67
|
+
return next();
|
|
77
68
|
const fileStats = statSync(file);
|
|
78
69
|
const extension = file.split('.').pop() || '';
|
|
79
70
|
const contentType = mimeTypes[`.${extension}`] || 'application/octet-stream';
|
|
@@ -103,22 +94,21 @@ export default class ZubyServer {
|
|
|
103
94
|
headers['Content-Length'] = (end - start + 1).toString();
|
|
104
95
|
headers['Accept-Ranges'] = 'bytes';
|
|
105
96
|
}
|
|
106
|
-
//
|
|
107
|
-
res.writeHead(statusCode,
|
|
97
|
+
// Write the headers
|
|
98
|
+
res.writeHead(statusCode, headers);
|
|
108
99
|
// Stream the file
|
|
109
100
|
createReadStream(file, streamOptions)
|
|
110
101
|
.pipe(res)
|
|
111
102
|
.on('finish', () => {
|
|
112
103
|
res.end();
|
|
113
104
|
});
|
|
114
|
-
return true;
|
|
115
105
|
}
|
|
116
|
-
async
|
|
106
|
+
async serverDirMiddleware(nodeReq, nodeRes) {
|
|
117
107
|
const req = await this.toRequest(nodeReq);
|
|
118
108
|
const res = await this.renderer.render(req);
|
|
119
109
|
return this.toNodeResponse(res, nodeRes);
|
|
120
110
|
}
|
|
121
|
-
async
|
|
111
|
+
async listen() {
|
|
122
112
|
const serverPromise = new Promise((resolve, reject) => {
|
|
123
113
|
this.server.listen(this.options.port, this.options.host, 511, () => {
|
|
124
114
|
if (!this.server.listening)
|
|
@@ -129,7 +119,7 @@ export default class ZubyServer {
|
|
|
129
119
|
const rendererPromise = this.renderer.init();
|
|
130
120
|
await Promise.all([serverPromise, rendererPromise]);
|
|
131
121
|
}
|
|
132
|
-
async
|
|
122
|
+
async close() {
|
|
133
123
|
this.server.closeAllConnections();
|
|
134
124
|
return new Promise((resolve, reject) => {
|
|
135
125
|
this.server.close(error => {
|
|
@@ -139,12 +129,21 @@ export default class ZubyServer {
|
|
|
139
129
|
});
|
|
140
130
|
});
|
|
141
131
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
132
|
+
defaultHeadersMiddleware(_req, res, next) {
|
|
133
|
+
res.setHeader('X-Powered-By', 'Zuby.js');
|
|
134
|
+
next();
|
|
135
|
+
}
|
|
136
|
+
trailingSlashMiddleware(req, res, next) {
|
|
137
|
+
// Redirect to path with trailing slash for consistency
|
|
138
|
+
const [path, query] = req.url?.split('?') || [];
|
|
139
|
+
// Do nothing if the path already has a trailing slash
|
|
140
|
+
// or if the path has file extension
|
|
141
|
+
if (path.endsWith('/') || path.match(/[.](.+)$/)) {
|
|
142
|
+
return next();
|
|
143
|
+
}
|
|
144
|
+
res.statusCode = 302;
|
|
145
|
+
res.setHeader('Location', `${path}/${query ? `?${query}` : ''}`);
|
|
146
|
+
res.end();
|
|
148
147
|
}
|
|
149
148
|
async toRequest(nodeReq) {
|
|
150
149
|
return new Promise((resolve, reject) => {
|
|
@@ -176,7 +175,16 @@ export default class ZubyServer {
|
|
|
176
175
|
async toNodeResponse(res, nodeRes) {
|
|
177
176
|
const headers = {};
|
|
178
177
|
res.headers.forEach((value, key) => (headers[key] = value));
|
|
179
|
-
nodeRes.writeHead(res.status,
|
|
178
|
+
nodeRes.writeHead(res.status, headers);
|
|
180
179
|
nodeRes.end(await res.text());
|
|
181
180
|
}
|
|
181
|
+
use(middleware) {
|
|
182
|
+
this.middlewares.push(middleware);
|
|
183
|
+
}
|
|
184
|
+
useBefore(middleware) {
|
|
185
|
+
this.middlewares.unshift(middleware);
|
|
186
|
+
}
|
|
187
|
+
async reload() {
|
|
188
|
+
await this.renderer.reload();
|
|
189
|
+
}
|
|
182
190
|
}
|