thunderous-server 0.0.3 → 0.0.5
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/dist/index.cjs +7 -2
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/package.json +3 -9
- package/src/build.ts +6 -1
- package/src/cli.ts +22 -39
- package/src/dev.ts +13 -136
- package/src/generate.ts +104 -34
- package/src/index.ts +1 -1
- package/src/vendorize.ts +8 -1
- package/src/vite-plugin.ts +202 -0
package/dist/index.cjs
CHANGED
|
@@ -22,7 +22,8 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
escapeHtml: () => escapeHtml,
|
|
24
24
|
getMeta: () => getMeta,
|
|
25
|
-
raw: () => raw
|
|
25
|
+
raw: () => raw,
|
|
26
|
+
setMeta: () => setMeta
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
|
|
@@ -40,6 +41,9 @@ var metaState = {
|
|
|
40
41
|
name: "",
|
|
41
42
|
filename: ""
|
|
42
43
|
};
|
|
44
|
+
var setMeta = (meta) => {
|
|
45
|
+
Object.assign(metaState, meta);
|
|
46
|
+
};
|
|
43
47
|
var getMeta = () => {
|
|
44
48
|
return Object.freeze({
|
|
45
49
|
...metaState,
|
|
@@ -66,5 +70,6 @@ var raw = (str) => new RawHtml(str);
|
|
|
66
70
|
0 && (module.exports = {
|
|
67
71
|
escapeHtml,
|
|
68
72
|
getMeta,
|
|
69
|
-
raw
|
|
73
|
+
raw,
|
|
74
|
+
setMeta
|
|
70
75
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -69,6 +69,8 @@ type Meta = {
|
|
|
69
69
|
*/
|
|
70
70
|
filename: string;
|
|
71
71
|
};
|
|
72
|
+
/** Set metadata context for the current page being rendered. */
|
|
73
|
+
declare const setMeta: (meta: Partial<Meta>) => void;
|
|
72
74
|
/** Get metadata about the current page being rendered. */
|
|
73
75
|
declare const getMeta: () => Meta;
|
|
74
76
|
|
|
@@ -86,4 +88,4 @@ declare class RawHtml {
|
|
|
86
88
|
*/
|
|
87
89
|
declare const raw: (str: string) => RawHtml;
|
|
88
90
|
|
|
89
|
-
export { escapeHtml, getMeta, raw };
|
|
91
|
+
export { escapeHtml, getMeta, raw, setMeta };
|
package/dist/index.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ type Meta = {
|
|
|
69
69
|
*/
|
|
70
70
|
filename: string;
|
|
71
71
|
};
|
|
72
|
+
/** Set metadata context for the current page being rendered. */
|
|
73
|
+
declare const setMeta: (meta: Partial<Meta>) => void;
|
|
72
74
|
/** Get metadata about the current page being rendered. */
|
|
73
75
|
declare const getMeta: () => Meta;
|
|
74
76
|
|
|
@@ -86,4 +88,4 @@ declare class RawHtml {
|
|
|
86
88
|
*/
|
|
87
89
|
declare const raw: (str: string) => RawHtml;
|
|
88
90
|
|
|
89
|
-
export { escapeHtml, getMeta, raw };
|
|
91
|
+
export { escapeHtml, getMeta, raw, setMeta };
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,9 @@ var metaState = {
|
|
|
12
12
|
name: "",
|
|
13
13
|
filename: ""
|
|
14
14
|
};
|
|
15
|
+
var setMeta = (meta) => {
|
|
16
|
+
Object.assign(metaState, meta);
|
|
17
|
+
};
|
|
15
18
|
var getMeta = () => {
|
|
16
19
|
return Object.freeze({
|
|
17
20
|
...metaState,
|
|
@@ -37,5 +40,6 @@ var raw = (str) => new RawHtml(str);
|
|
|
37
40
|
export {
|
|
38
41
|
escapeHtml,
|
|
39
42
|
getMeta,
|
|
40
|
-
raw
|
|
43
|
+
raw,
|
|
44
|
+
setMeta
|
|
41
45
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunderous-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "A simple server to enhance Thunderous components with SSR.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan DeWitt <jon.dewitt@thunder.solutions>",
|
|
@@ -24,9 +24,6 @@
|
|
|
24
24
|
"@eslint/js": "^9.38.0",
|
|
25
25
|
"@eslint/json": "^0.13.2",
|
|
26
26
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
27
|
-
"@types/connect-livereload": "^0.6.3",
|
|
28
|
-
"@types/express": "^5.0.3",
|
|
29
|
-
"@types/livereload": "^0.9.5",
|
|
30
27
|
"@types/node": "^24.9.1",
|
|
31
28
|
"@types/resolve": "^1.20.6",
|
|
32
29
|
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
|
@@ -40,16 +37,13 @@
|
|
|
40
37
|
},
|
|
41
38
|
"dependencies": {
|
|
42
39
|
"chalk": "^5.6.2",
|
|
43
|
-
"connect-livereload": "^0.6.1",
|
|
44
40
|
"emoji-space-shim": "^0.1.7",
|
|
45
41
|
"es-module-lexer": "^1.7.0",
|
|
46
|
-
"express": "^5.1.0",
|
|
47
|
-
"livereload": "^0.10.3",
|
|
48
42
|
"node-emoji": "^2.2.0",
|
|
49
|
-
"nodemon": "^3.1.14",
|
|
50
43
|
"resolve": "^1.22.11",
|
|
51
44
|
"resolve.exports": "^2.0.3",
|
|
52
|
-
"string-width": "^8.2.0"
|
|
45
|
+
"string-width": "^8.2.0",
|
|
46
|
+
"vite": "^6.3.5"
|
|
53
47
|
},
|
|
54
48
|
"peerDependencies": {
|
|
55
49
|
"thunderous": ">=2.4.2"
|
package/src/build.ts
CHANGED
|
@@ -42,7 +42,12 @@ export const build = () => {
|
|
|
42
42
|
|
|
43
43
|
cpSync(baseDir, outDir, {
|
|
44
44
|
recursive: true,
|
|
45
|
-
filter: (src) =>
|
|
45
|
+
filter: (src) => {
|
|
46
|
+
const name = src.split('/').pop() ?? '';
|
|
47
|
+
if (name.startsWith('_')) return false;
|
|
48
|
+
if (/\.server\.(ts|mts|cts|tsx|js|mjs|cjs|jsx)$/.test(name)) return false;
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
46
51
|
});
|
|
47
52
|
bootstrapThunderous();
|
|
48
53
|
|
package/src/cli.ts
CHANGED
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
import { build } from './build';
|
|
2
|
-
import nodemon from 'nodemon';
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
4
|
-
import { config } from './config';
|
|
5
|
-
import { relative, resolve } from 'path';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
2
|
|
|
8
3
|
const args = process.argv.slice(2);
|
|
9
4
|
|
|
10
5
|
if (args[0] === 'dev') {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
exec: 'tsx',
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
nodemon
|
|
28
|
-
.on('start', () => {
|
|
29
|
-
// console.log('App has started');
|
|
30
|
-
})
|
|
31
|
-
.on('quit', () => {
|
|
32
|
-
console.log(chalk.green('\nServer shut down successfully.\n'));
|
|
33
|
-
process.exit();
|
|
34
|
-
})
|
|
35
|
-
.on('restart', (files) => {
|
|
36
|
-
const filesList = (files ?? ['(none)']).map(
|
|
37
|
-
(file) => ` • ${chalk.cyan.underline(relative(config.configDir ?? process.cwd(), file))}\n`,
|
|
38
|
-
);
|
|
39
|
-
console.log('\nUpdates detected:\n', filesList.join(''));
|
|
40
|
-
});
|
|
6
|
+
// Parse port from command line arguments
|
|
7
|
+
process.env.PORT = process.env.PORT ?? '3000';
|
|
8
|
+
args.forEach((arg, i) => {
|
|
9
|
+
if (arg.startsWith('--port')) {
|
|
10
|
+
let port: string | undefined;
|
|
11
|
+
if (arg.includes('=')) {
|
|
12
|
+
port = arg.split('=')[1];
|
|
13
|
+
} else {
|
|
14
|
+
port = args[i + 1];
|
|
15
|
+
}
|
|
16
|
+
if (port !== '' && port !== undefined) {
|
|
17
|
+
process.env.PORT = port;
|
|
18
|
+
}
|
|
41
19
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Vite handles HMR, port finding, and file watching
|
|
23
|
+
import('./dev')
|
|
24
|
+
.then(({ dev }) => dev())
|
|
25
|
+
.catch((error) => {
|
|
26
|
+
console.error('\x1b[31mFailed to start server:\x1b[0m', error);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
46
29
|
} else if (args[0] === 'build') {
|
|
47
30
|
try {
|
|
48
31
|
build();
|
package/src/dev.ts
CHANGED
|
@@ -1,140 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { join, relative, resolve } from 'path';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
import { ModuleKind, ScriptTarget, transpileModule, ImportsNotUsedAsValues } from 'typescript';
|
|
6
|
-
import { bootstrapThunderous, generateStaticTemplate, generateImportMap, injectImportMap } from './generate';
|
|
1
|
+
import { createServer } from 'vite';
|
|
2
|
+
import { resolve } from 'path';
|
|
7
3
|
import { config } from './config';
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const dirPath = resolve(dir);
|
|
18
|
-
const files = readdirSync(dirPath);
|
|
19
|
-
|
|
20
|
-
for (const file of files) {
|
|
21
|
-
const filePath = join(dirPath, file);
|
|
22
|
-
if (file.endsWith('.html') && !file.startsWith('_')) {
|
|
23
|
-
const basePath = relative(config.baseDir, dirPath);
|
|
24
|
-
const path = `/${basePath}${file === 'index.html' ? '' : `/${file.replace('.html', '')}`}`;
|
|
25
|
-
console.log(`\x1b[90mFound path: ${path}\x1b[0m`);
|
|
26
|
-
|
|
27
|
-
app.get(path, (_, res) => {
|
|
28
|
-
console.log(`\x1b[90mServing file: ${basePath}/${file}\x1b[0m`);
|
|
29
|
-
|
|
30
|
-
// Process the HTML file and extract client entry files
|
|
31
|
-
const result = generateStaticTemplate(filePath);
|
|
32
|
-
let markup = result.markup;
|
|
33
|
-
|
|
34
|
-
// Generate import map if there are client entry files
|
|
35
|
-
if (result.clientEntryFiles.length > 0) {
|
|
36
|
-
const importMapJson = generateImportMap(result.clientEntryFiles, vendorDir);
|
|
37
|
-
markup = injectImportMap(markup, importMapJson);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Clean up temporary files
|
|
41
|
-
result.cleanup();
|
|
42
|
-
|
|
43
|
-
res.send(markup);
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
if (statSync(filePath).isDirectory()) {
|
|
47
|
-
bootstrapRoutes(filePath, app, vendorDir);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const dev = () => {
|
|
53
|
-
console.log('\n\x1b[36m\x1b[1m⚡⚡ Starting development server... ⚡⚡\x1b[0m\x1b[0m\n');
|
|
54
|
-
|
|
55
|
-
const PORT = process.env.PORT ?? 3000;
|
|
56
|
-
|
|
57
|
-
// Set up simple express server
|
|
58
|
-
const app = express();
|
|
59
|
-
|
|
60
|
-
// Set up live reload to watch for changes
|
|
61
|
-
const liveReloadServer = livereload.createServer();
|
|
62
|
-
liveReloadServer.watch(`${process.cwd()}/${config.baseDir}`);
|
|
63
|
-
app.use(connectLiveReload());
|
|
64
|
-
app.use((_, res, next) => {
|
|
65
|
-
const originalSend = res.send;
|
|
66
|
-
|
|
67
|
-
// Inject live reload script into HTML responses
|
|
68
|
-
res.send = function (body) {
|
|
69
|
-
if (typeof body === 'string' && body.includes('</body>')) {
|
|
70
|
-
const liveReloadScript = '<script src="http://localhost:35729/livereload.js"></script>';
|
|
71
|
-
body = body.replace('</body>', `${liveReloadScript}</body>`);
|
|
72
|
-
}
|
|
73
|
-
return originalSend.call(this, body);
|
|
74
|
-
};
|
|
75
|
-
next();
|
|
4
|
+
import { thunderousPlugin } from './vite-plugin';
|
|
5
|
+
|
|
6
|
+
export const dev = async () => {
|
|
7
|
+
const server = await createServer({
|
|
8
|
+
root: resolve(config.baseDir),
|
|
9
|
+
plugins: [thunderousPlugin()],
|
|
10
|
+
server: {
|
|
11
|
+
port: Number(process.env.PORT ?? '3000'),
|
|
12
|
+
},
|
|
76
13
|
});
|
|
77
14
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.log(`\x1b[90mUsing temporary vendor directory: ${vendorDir}\x1b[0m`);
|
|
81
|
-
|
|
82
|
-
bootstrapThunderous();
|
|
83
|
-
bootstrapRoutes(`./${config.baseDir}`, app, vendorDir);
|
|
84
|
-
|
|
85
|
-
// Serve .js requests by transpiling the corresponding .ts source on-the-fly
|
|
86
|
-
app.use((req, res, next) => {
|
|
87
|
-
if (!req.path.endsWith('.js')) return next();
|
|
88
|
-
const tsPath = join(config.baseDir, req.path.replace(/\.js$/, '.ts'));
|
|
89
|
-
const tsxPath = join(config.baseDir, req.path.replace(/\.js$/, '.tsx'));
|
|
90
|
-
const srcPath = existsSync(tsPath) ? tsPath : existsSync(tsxPath) ? tsxPath : null;
|
|
91
|
-
if (srcPath === null) return next();
|
|
92
|
-
const source = readFileSync(srcPath, 'utf-8');
|
|
93
|
-
const { outputText } = transpileModule(source, {
|
|
94
|
-
compilerOptions: {
|
|
95
|
-
module: ModuleKind.ESNext,
|
|
96
|
-
target: ScriptTarget.ES2020,
|
|
97
|
-
sourceMap: false,
|
|
98
|
-
importsNotUsedAsValues: ImportsNotUsedAsValues.Remove,
|
|
99
|
-
verbatimModuleSyntax: true,
|
|
100
|
-
isolatedModules: true,
|
|
101
|
-
},
|
|
102
|
-
fileName: srcPath,
|
|
103
|
-
reportDiagnostics: false,
|
|
104
|
-
});
|
|
105
|
-
res.type('application/javascript').send(outputText);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Serve static assets from the base directory
|
|
109
|
-
app.use(express.static(config.baseDir));
|
|
110
|
-
|
|
111
|
-
// Serve vendorized modules
|
|
112
|
-
app.use('/vendor', express.static(join(vendorDir, 'vendor')));
|
|
113
|
-
|
|
114
|
-
const server = app.listen(PORT, () => {
|
|
115
|
-
console.log(`\n\x1b[38;2;100;149;237mServer is running on http://localhost:${PORT}\x1b[0m\n`);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Close everything when server is closed
|
|
119
|
-
server.once('close', () => {
|
|
120
|
-
liveReloadServer.close();
|
|
121
|
-
server.closeAllConnections?.();
|
|
122
|
-
console.log('\x1b[32m\nAll connections closed successfully.\x1b[0m\n\n');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Handle graceful shutdown when user cancels the process
|
|
126
|
-
process.once('SIGINT', () => {
|
|
127
|
-
console.log('\n\x1b[90mShutting down gracefully...\x1b[0m');
|
|
128
|
-
server.close((error) => {
|
|
129
|
-
if (error === undefined) {
|
|
130
|
-
process.exitCode = 0;
|
|
131
|
-
} else {
|
|
132
|
-
console.error('Error closing server:', error);
|
|
133
|
-
process.exitCode = 1;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
});
|
|
15
|
+
await server.listen();
|
|
16
|
+
server.printUrls();
|
|
137
17
|
};
|
|
138
|
-
|
|
139
|
-
// start directly, since this file is targeted directly by nodemon
|
|
140
|
-
dev();
|
package/src/generate.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, wri
|
|
|
2
2
|
import { join, relative, resolve } from 'path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { html } from 'thunderous';
|
|
5
|
-
import {
|
|
5
|
+
import { type Breadcrumb } from './meta';
|
|
6
6
|
import { basename, dirname, extname } from 'node:path';
|
|
7
7
|
import { config } from './config';
|
|
8
8
|
import { ModuleKind, ScriptTarget, transpileModule, ImportsNotUsedAsValues } from 'typescript';
|
|
@@ -51,15 +51,32 @@ export const bootstrapThunderous = () => {
|
|
|
51
51
|
innerHTML.replace(/\s+/gm, ' ').replace(/ >/g, '>'),
|
|
52
52
|
renderState.markup,
|
|
53
53
|
);
|
|
54
|
-
console.log(`\x1b[
|
|
54
|
+
console.log(`\x1b[90m |--- Inserted SSR template for <${tagName}> into markup.\x1b[0m`);
|
|
55
55
|
});
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
/** Rewrite relative import specifiers so the browser can resolve them (e.g. '../theme' → '../theme.js'). */
|
|
59
|
+
const rewriteRelativeImports = (code: string, jsFilePath: string): string =>
|
|
60
|
+
code.replace(
|
|
61
|
+
/((?:^|[\s;,])(?:import|export)\s.*?from\s+['"])([^'"]+)(['"])/gm,
|
|
62
|
+
(_match, prefix: string, spec: string, suffix: string) => {
|
|
63
|
+
if (!spec.startsWith('.')) return `${prefix}${spec}${suffix}`;
|
|
64
|
+
if (/\.[a-z]+$/i.test(spec)) return `${prefix}${spec}${suffix}`;
|
|
65
|
+
const dir = dirname(jsFilePath);
|
|
66
|
+
if (existsSync(join(dir, spec + '.js')) || existsSync(join(dir, spec, 'index.js'))) {
|
|
67
|
+
const resolved = existsSync(join(dir, spec + '.js')) ? spec + '.js' : spec + '/index.js';
|
|
68
|
+
return `${prefix}${resolved}${suffix}`;
|
|
69
|
+
}
|
|
70
|
+
return `${prefix}${spec}.js${suffix}`;
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
58
74
|
/** Transpile a .ts file on disk to .js, writing the output next to it. Returns the .js path. */
|
|
59
75
|
export const transpileTsFile = (tsFilePath: string) => {
|
|
60
76
|
const source = readFileSync(tsFilePath, 'utf-8');
|
|
61
|
-
|
|
77
|
+
let js = transpileTs(source, basename(tsFilePath));
|
|
62
78
|
const jsPath = tsFilePath.replace(/\.ts$/, '.js');
|
|
79
|
+
js = rewriteRelativeImports(js, jsPath);
|
|
63
80
|
writeFileSync(jsPath, js, 'utf-8');
|
|
64
81
|
rmSync(tsFilePath);
|
|
65
82
|
return jsPath;
|
|
@@ -90,7 +107,7 @@ export const processFiles = (args: ProcessFilesArgs) => {
|
|
|
90
107
|
};
|
|
91
108
|
|
|
92
109
|
type ScriptKind = 'expr' | 'server' | 'isomorphic' | 'module';
|
|
93
|
-
type ParsedScript = { kind: ScriptKind; content: string;
|
|
110
|
+
type ParsedScript = { kind: ScriptKind; content: string; src?: string | undefined; start: number; end: number };
|
|
94
111
|
type ParsedLayout = { href: string; start: number; end: number };
|
|
95
112
|
|
|
96
113
|
// ── Cached resolved paths (computed once at module load) ──
|
|
@@ -150,9 +167,9 @@ const extractTags = (markup: string) => {
|
|
|
150
167
|
else if (/\bisomorphic\b/.test(attrs)) kind = 'isomorphic';
|
|
151
168
|
else if (/\btype\s*=\s*"module"/.test(attrs)) kind = 'module';
|
|
152
169
|
|
|
153
|
-
let
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
170
|
+
let src: string | undefined;
|
|
171
|
+
const srcMatch = /\bsrc\s*=\s*"([^"]*)"/.exec(attrs);
|
|
172
|
+
if (srcMatch) src = srcMatch[1];
|
|
156
173
|
|
|
157
174
|
let endPos = -1;
|
|
158
175
|
let j = tagClose + 1;
|
|
@@ -172,7 +189,7 @@ const extractTags = (markup: string) => {
|
|
|
172
189
|
|
|
173
190
|
if (kind !== null) {
|
|
174
191
|
const content = markup.slice(tagClose + 1, markup.lastIndexOf('</', endPos - 1)).trim();
|
|
175
|
-
scripts.push({ kind, content,
|
|
192
|
+
scripts.push({ kind, content, src, start: i, end: endPos });
|
|
176
193
|
}
|
|
177
194
|
i = endPos;
|
|
178
195
|
continue;
|
|
@@ -212,38 +229,44 @@ const extractTags = (markup: string) => {
|
|
|
212
229
|
* ```
|
|
213
230
|
*/
|
|
214
231
|
export const generateStaticTemplate = (filePath: string) => {
|
|
232
|
+
const configDir = config.configDir ?? process.cwd();
|
|
233
|
+
console.log(`Building: ${relative(configDir, filePath)}`);
|
|
215
234
|
renderState.markup = readFileSync(filePath, 'utf-8');
|
|
216
235
|
renderState.insertedTags.clear();
|
|
217
236
|
|
|
218
237
|
const name = basename(filePath, extname(filePath));
|
|
219
238
|
|
|
239
|
+
// quick local utility to convert kebab/camel/snake case to title case
|
|
240
|
+
const toTitleFormat = (str: string) =>
|
|
241
|
+
str
|
|
242
|
+
.replace(/[-_]|(?<=[a-z])(?=[A-Z])/g, ' ')
|
|
243
|
+
.replace(/(?:\b|^)\w/g, (m) => m.toUpperCase())
|
|
244
|
+
.trim();
|
|
245
|
+
|
|
220
246
|
// Set metadata context for the current page before server scripts run
|
|
221
247
|
const relativePath = relative(resolvedBaseDir, filePath);
|
|
222
248
|
const parentDir = dirname(relativePath).replace(/^\./, '');
|
|
223
249
|
const pathname = `/${parentDir}${name === 'index' ? '' : `/${name}`}`;
|
|
224
250
|
const titleWord = name === 'index' ? (pathname.split('/').pop() ?? '') : name;
|
|
225
|
-
const title = titleWord
|
|
226
|
-
.split(/[-_]/)
|
|
227
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
228
|
-
.join(' ');
|
|
251
|
+
const title = toTitleFormat(titleWord);
|
|
229
252
|
const path = pathname.split('/').filter((segment) => segment !== '');
|
|
230
|
-
let crumbPathname = '/';
|
|
231
253
|
const breadcrumbs: Breadcrumb[] = [
|
|
232
254
|
{
|
|
233
255
|
// always add the home page
|
|
234
|
-
name: config.name,
|
|
235
|
-
pathname:
|
|
256
|
+
name: toTitleFormat(config.name),
|
|
257
|
+
pathname: '/',
|
|
236
258
|
},
|
|
237
259
|
];
|
|
238
260
|
// add each segment of the path as a breadcrumb
|
|
261
|
+
let crumbPathname = '';
|
|
239
262
|
for (const segment of path) {
|
|
240
263
|
crumbPathname += `/${segment}`;
|
|
241
264
|
breadcrumbs.push({
|
|
242
|
-
name: segment,
|
|
265
|
+
name: toTitleFormat(segment),
|
|
243
266
|
pathname: crumbPathname,
|
|
244
267
|
});
|
|
245
268
|
}
|
|
246
|
-
setMeta({
|
|
269
|
+
outRequire('thunderous-server').setMeta({
|
|
247
270
|
config,
|
|
248
271
|
pathname,
|
|
249
272
|
title,
|
|
@@ -276,7 +299,9 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
276
299
|
}
|
|
277
300
|
renderState.markup =
|
|
278
301
|
layoutContent.slice(0, slotIndex) + renderState.markup + layoutContent.slice(slotIndex + slotTag.length);
|
|
279
|
-
|
|
302
|
+
|
|
303
|
+
const absLayoutPath = resolve(dirname(filePath), layout.href);
|
|
304
|
+
console.log(`\x1b[90m | Applied layout: ${relative(configDir, absLayoutPath)}\x1b[0m`);
|
|
280
305
|
}
|
|
281
306
|
// ── Extract and process scripts ──
|
|
282
307
|
const { scripts } = extractTags(renderState.markup);
|
|
@@ -286,7 +311,7 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
286
311
|
const tempFilesToCleanup: string[] = [];
|
|
287
312
|
|
|
288
313
|
// Map from scriptKey → replacement text (built during processing, applied later)
|
|
289
|
-
const scriptKey = (s: ParsedScript) => `${s.kind}|${s.
|
|
314
|
+
const scriptKey = (s: ParsedScript) => `${s.kind}|${s.src ?? ''}|${s.content}`;
|
|
290
315
|
const replacementMap = new Map<string, string>();
|
|
291
316
|
|
|
292
317
|
mkdirSync(resolvedOutDir, { recursive: true });
|
|
@@ -295,17 +320,21 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
295
320
|
for (const script of scripts) {
|
|
296
321
|
const key = scriptKey(script);
|
|
297
322
|
|
|
298
|
-
// ── Resolve content: from
|
|
323
|
+
// ── Resolve content: from src file or inline ──
|
|
299
324
|
let content = script.content;
|
|
300
|
-
let
|
|
301
|
-
if (script.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
325
|
+
let srcAbsPath: string | undefined;
|
|
326
|
+
if (script.src) {
|
|
327
|
+
if (script.src.startsWith('/')) {
|
|
328
|
+
srcAbsPath = resolve(resolvedBaseDir, script.src.slice(1));
|
|
329
|
+
} else {
|
|
330
|
+
srcAbsPath = resolve(fileDir, script.src);
|
|
331
|
+
}
|
|
332
|
+
if (!existsSync(srcAbsPath)) {
|
|
333
|
+
console.warn(`\x1b[33mWarning: Script src file not found: ${srcAbsPath}\x1b[0m`);
|
|
305
334
|
continue;
|
|
306
335
|
}
|
|
307
336
|
if (script.kind === 'expr') {
|
|
308
|
-
content = readFileSync(
|
|
337
|
+
content = readFileSync(srcAbsPath, 'utf-8').trim();
|
|
309
338
|
}
|
|
310
339
|
}
|
|
311
340
|
|
|
@@ -318,10 +347,10 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
318
347
|
// ── Server/isomorphic: execute server-side to collect exported values ──
|
|
319
348
|
if (script.kind === 'server' || script.kind === 'isomorphic') {
|
|
320
349
|
let module: Record<string, unknown>;
|
|
321
|
-
if (
|
|
350
|
+
if (srcAbsPath) {
|
|
322
351
|
// href: require the file directly from source
|
|
323
|
-
delete outRequire.cache[outRequire.resolve(
|
|
324
|
-
module = outRequire(
|
|
352
|
+
delete outRequire.cache[outRequire.resolve(srcAbsPath)];
|
|
353
|
+
module = outRequire(srcAbsPath);
|
|
325
354
|
} else {
|
|
326
355
|
// inline: write temp .ts into baseDir so relative imports resolve correctly
|
|
327
356
|
const tsScriptFile = join(resolvedBaseDir, `${name}-${scriptIndex}.tmp.ts`);
|
|
@@ -341,12 +370,12 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
341
370
|
if (script.kind === 'server') {
|
|
342
371
|
// Server scripts are fully discarded from client output
|
|
343
372
|
replacementMap.set(key, '');
|
|
344
|
-
} else if (
|
|
373
|
+
} else if (srcAbsPath) {
|
|
345
374
|
// href isomorphic/module: output a <script src> pointing to the .js file
|
|
346
|
-
const jsSrc = script.
|
|
375
|
+
const jsSrc = script.src!.replace(/\.tsx?$/, '.js');
|
|
347
376
|
replacementMap.set(key, `<script type="module" src="${jsSrc}"></script>`);
|
|
348
377
|
// Resolve the outDir .js path for vendorization
|
|
349
|
-
const hrefRelPath = relative(resolvedBaseDir,
|
|
378
|
+
const hrefRelPath = relative(resolvedBaseDir, srcAbsPath);
|
|
350
379
|
const hrefOutJsPath = join(resolvedOutDir, hrefRelPath).replace(/\.tsx?$/, '.js');
|
|
351
380
|
clientEntryFiles.push(resolve(hrefOutJsPath));
|
|
352
381
|
} else {
|
|
@@ -370,6 +399,16 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
370
399
|
scriptIndex++;
|
|
371
400
|
}
|
|
372
401
|
|
|
402
|
+
// ── Pick up any new scripts added by template insertion (e.g. expr inside components) ──
|
|
403
|
+
const { scripts: postInsertScripts } = extractTags(renderState.markup);
|
|
404
|
+
for (const script of postInsertScripts) {
|
|
405
|
+
const key = scriptKey(script);
|
|
406
|
+
if (replacementMap.has(key)) continue;
|
|
407
|
+
if (script.kind === 'expr') {
|
|
408
|
+
replacementMap.set(key, '__expr__');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
373
412
|
// ── Re-parse and apply all replacements in one reverse pass ──
|
|
374
413
|
const { scripts: freshScripts } = extractTags(renderState.markup);
|
|
375
414
|
for (let i = freshScripts.length - 1; i >= 0; i--) {
|
|
@@ -383,8 +422,8 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
383
422
|
if (replacement === '__expr__') {
|
|
384
423
|
// Evaluate expr: content may come from href file
|
|
385
424
|
let content = script.content;
|
|
386
|
-
if (script.
|
|
387
|
-
const hrefPath = resolve(fileDir, script.
|
|
425
|
+
if (script.src) {
|
|
426
|
+
const hrefPath = resolve(fileDir, script.src);
|
|
388
427
|
content = readFileSync(hrefPath, 'utf-8').trim();
|
|
389
428
|
}
|
|
390
429
|
const expression = content.replace(/;$/, '');
|
|
@@ -401,7 +440,38 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
401
440
|
}
|
|
402
441
|
renderState.markup = renderState.markup.slice(0, script.start) + text + renderState.markup.slice(script.end);
|
|
403
442
|
}
|
|
443
|
+
// ── Evaluate expr: attributes ──
|
|
444
|
+
renderState.markup = renderState.markup.replace(
|
|
445
|
+
/(<[a-zA-Z][\w-]*\b)((?:\s+[^>]*?)?)(\s*\/?>)/g,
|
|
446
|
+
(_match, openTag: string, attrs: string, close: string) => {
|
|
447
|
+
if (!attrs.includes('expr:')) return _match;
|
|
448
|
+
const result = attrs.replace(
|
|
449
|
+
/\s+expr:([a-zA-Z][\w-]*)=(["'])([\s\S]*?)\2/g,
|
|
450
|
+
(_attrMatch: string, attrName: string, _quote: string, expression: string) => {
|
|
451
|
+
try {
|
|
452
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
453
|
+
const value = Function(
|
|
454
|
+
'html',
|
|
455
|
+
'escapeHtml',
|
|
456
|
+
'raw',
|
|
457
|
+
...Object.keys(safeValues),
|
|
458
|
+
`'use strict'; return (${expression});`,
|
|
459
|
+
)(html, escapeHtml, raw, ...Object.values(safeValues));
|
|
460
|
+
if (value == null || value === false || value === '') return '';
|
|
461
|
+
if (value === true) return ` ${attrName}`;
|
|
462
|
+
return ` ${attrName}="${escapeHtml(String(value))}"`;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.warn(`\x1b[33mWarning: Failed to evaluate expr:${attrName}="${expression}":\x1b[0m`, e);
|
|
465
|
+
return '';
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
return openTag + result + close;
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
|
|
404
473
|
renderState.markup = renderState.markup.trim();
|
|
474
|
+
console.log('');
|
|
405
475
|
|
|
406
476
|
// Return the final rendered markup, client entry files, and cleanup function
|
|
407
477
|
return {
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getMeta } from './meta';
|
|
1
|
+
export { getMeta, setMeta } from './meta';
|
|
2
2
|
export { escapeHtml, raw } from './utilities';
|
package/src/vendorize.ts
CHANGED
|
@@ -38,12 +38,19 @@ export function processNodeModules(entryFiles: string[], outputDir?: string) {
|
|
|
38
38
|
} else {
|
|
39
39
|
const tsFile = file.replace(/\.js$/, '.ts');
|
|
40
40
|
const tsxFile = file.replace(/\.js$/, '.tsx');
|
|
41
|
+
const srcFile = file.replace(`/${config.outDir}/`, `/${config.baseDir}/`);
|
|
42
|
+
const srcTsFile = srcFile.replace(/\.js$/, '.ts');
|
|
43
|
+
const srcTsxFile = srcFile.replace(/\.js$/, '.tsx');
|
|
41
44
|
if (existsSync(tsFile)) {
|
|
42
45
|
code = transpileTsFast(readFileSync(tsFile, 'utf8'), tsFile);
|
|
43
46
|
} else if (existsSync(tsxFile)) {
|
|
44
47
|
code = transpileTsFast(readFileSync(tsxFile, 'utf8'), tsxFile);
|
|
48
|
+
} else if (existsSync(srcTsFile)) {
|
|
49
|
+
code = transpileTsFast(readFileSync(srcTsFile, 'utf8'), srcTsFile);
|
|
50
|
+
} else if (existsSync(srcTsxFile)) {
|
|
51
|
+
code = transpileTsFast(readFileSync(srcTsxFile, 'utf8'), srcTsxFile);
|
|
45
52
|
} else {
|
|
46
|
-
throw new Error(`Cannot find module: ${file} (also tried .ts/.tsx)`);
|
|
53
|
+
throw new Error(`Cannot find module: ${file} (also tried .ts/.tsx and ${config.baseDir}/ equivalents)`);
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
const [imports] = parse(code);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Plugin, ViteDevServer, Connect } from 'vite';
|
|
2
|
+
import type { ServerResponse } from 'http';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
5
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
6
|
+
import { bootstrapThunderous, generateStaticTemplate } from './generate';
|
|
7
|
+
import { config } from './config';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a URL pathname to an HTML file in the base directory.
|
|
11
|
+
* Maps routes like `/about` → `src/about/index.html`, `/about/contact` → `src/about/contact.html`.
|
|
12
|
+
*/
|
|
13
|
+
const resolveHtmlPath = (url: string, root: string): string | null => {
|
|
14
|
+
const pathname = url.split('?')[0]?.split('#')[0] ?? '/';
|
|
15
|
+
|
|
16
|
+
// Files starting with _ are excluded (layouts, partials, etc.)
|
|
17
|
+
const lastSegment = pathname.split('/').pop() ?? '';
|
|
18
|
+
if (lastSegment.startsWith('_')) return null;
|
|
19
|
+
|
|
20
|
+
// Try: /about → src/about/index.html
|
|
21
|
+
const indexPath = join(root, pathname, 'index.html');
|
|
22
|
+
if (existsSync(indexPath)) return indexPath;
|
|
23
|
+
|
|
24
|
+
// Try: /about/contact → src/about/contact.html
|
|
25
|
+
const directPath = join(root, pathname + '.html');
|
|
26
|
+
if (existsSync(directPath)) return directPath;
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Walk baseDir and collect all routable HTML files (excluding _layout, _partials, etc.).
|
|
33
|
+
* Returns an array of { filePath, urlPath } objects.
|
|
34
|
+
*/
|
|
35
|
+
const collectHtmlPages = (root: string): Array<{ filePath: string; urlPath: string }> => {
|
|
36
|
+
const pages: Array<{ filePath: string; urlPath: string }> = [];
|
|
37
|
+
const walk = (dir: string) => {
|
|
38
|
+
for (const entry of readdirSync(dir)) {
|
|
39
|
+
const full = join(dir, entry);
|
|
40
|
+
if (statSync(full).isDirectory()) {
|
|
41
|
+
walk(full);
|
|
42
|
+
} else if (entry.endsWith('.html') && !entry.startsWith('_')) {
|
|
43
|
+
const rel = relative(root, dirname(full));
|
|
44
|
+
const urlPath = `/${rel}${entry === 'index.html' ? '' : `/${entry.replace('.html', '')}`}`;
|
|
45
|
+
pages.push({ filePath: full, urlPath });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
walk(root);
|
|
50
|
+
return pages;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Vite plugin for Thunderous server-side rendering.
|
|
55
|
+
*
|
|
56
|
+
* Handles:
|
|
57
|
+
* - HTML page requests routed through `generateStaticTemplate`
|
|
58
|
+
* - `.js` → `.ts`/`.tsx` resolution for isomorphic script src attributes
|
|
59
|
+
* - Pre-renders all pages on file change BEFORE triggering browser reload
|
|
60
|
+
*/
|
|
61
|
+
export const thunderousPlugin = (): Plugin => {
|
|
62
|
+
const root = resolve(config.baseDir);
|
|
63
|
+
const outRequire = createRequire(resolve(config.baseDir));
|
|
64
|
+
|
|
65
|
+
// Pre-rendered SSR markup cache: urlPath → markup
|
|
66
|
+
const pageCache = new Map<string, string>();
|
|
67
|
+
|
|
68
|
+
const invalidateRequireCache = () => {
|
|
69
|
+
for (const key of Object.keys(outRequire.cache)) {
|
|
70
|
+
if (key.startsWith(root)) {
|
|
71
|
+
delete outRequire.cache[key];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Pre-render every routable page so SSR markup is ready before reload. */
|
|
77
|
+
const renderAllPages = () => {
|
|
78
|
+
pageCache.clear();
|
|
79
|
+
for (const { filePath, urlPath } of collectHtmlPages(root)) {
|
|
80
|
+
try {
|
|
81
|
+
const result = generateStaticTemplate(filePath);
|
|
82
|
+
pageCache.set(urlPath, result.markup);
|
|
83
|
+
result.cleanup();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`\x1b[31mError pre-rendering ${urlPath}:\x1b[0m`, error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const virtualId = 'virtual:thunderous-hmr-client';
|
|
91
|
+
const resolvedVirtualId = '\0' + virtualId;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
name: 'thunderous',
|
|
95
|
+
|
|
96
|
+
// resolveId(id) {
|
|
97
|
+
// if (id === virtualId) return resolvedVirtualId;
|
|
98
|
+
// return id;
|
|
99
|
+
// },
|
|
100
|
+
|
|
101
|
+
// load(id) {
|
|
102
|
+
// if (id === resolvedVirtualId) {
|
|
103
|
+
// return `
|
|
104
|
+
// if (import.meta.hot) {
|
|
105
|
+
// import.meta.hot.on('thunderous:reload', () => {
|
|
106
|
+
// location.reload();
|
|
107
|
+
// })
|
|
108
|
+
// }
|
|
109
|
+
// `;
|
|
110
|
+
// }
|
|
111
|
+
// return '';
|
|
112
|
+
// },
|
|
113
|
+
|
|
114
|
+
// transformIndexHtml(html) {
|
|
115
|
+
// return {
|
|
116
|
+
// html,
|
|
117
|
+
// tags: [
|
|
118
|
+
// {
|
|
119
|
+
// tag: 'script',
|
|
120
|
+
// attrs: { type: 'module' },
|
|
121
|
+
// children: `import "virtual:thunderous-hmr-client"`,
|
|
122
|
+
// injectTo: 'head',
|
|
123
|
+
// },
|
|
124
|
+
// ],
|
|
125
|
+
// };
|
|
126
|
+
// },
|
|
127
|
+
|
|
128
|
+
configureServer(server: ViteDevServer) {
|
|
129
|
+
bootstrapThunderous();
|
|
130
|
+
|
|
131
|
+
// Initial pre-render so the first request is served from cache.
|
|
132
|
+
renderAllPages();
|
|
133
|
+
|
|
134
|
+
// On any file change in baseDir: bust cache → re-render all pages → reload.
|
|
135
|
+
// All three steps are synchronous, so the pre-render logs appear before the reload.
|
|
136
|
+
const onFileChange = (file: string) => {
|
|
137
|
+
if (!file.startsWith(root)) return;
|
|
138
|
+
invalidateRequireCache();
|
|
139
|
+
renderAllPages();
|
|
140
|
+
server.ws.send({ type: 'full-reload' });
|
|
141
|
+
};
|
|
142
|
+
server.watcher.on('change', onFileChange);
|
|
143
|
+
server.watcher.on('add', onFileChange);
|
|
144
|
+
|
|
145
|
+
// Rewrite .js requests to .ts/.tsx when the .js file doesn't exist
|
|
146
|
+
// (generateStaticTemplate outputs .js src paths but source files are .ts)
|
|
147
|
+
server.middlewares.use((req: Connect.IncomingMessage, _res: unknown, next: Connect.NextFunction) => {
|
|
148
|
+
if (!req.url?.endsWith('.js')) return next();
|
|
149
|
+
const cleanUrl = req.url.split('?')[0] ?? '';
|
|
150
|
+
const jsPath = join(root, cleanUrl);
|
|
151
|
+
if (existsSync(jsPath)) return next();
|
|
152
|
+
const tsPath = join(root, cleanUrl.replace(/\.js$/, '.ts'));
|
|
153
|
+
const tsxPath = join(root, cleanUrl.replace(/\.js$/, '.tsx'));
|
|
154
|
+
if (existsSync(tsPath)) {
|
|
155
|
+
req.url = req.url.replace(/\.js$/, '.ts');
|
|
156
|
+
} else if (existsSync(tsxPath)) {
|
|
157
|
+
req.url = req.url.replace(/\.js$/, '.tsx');
|
|
158
|
+
}
|
|
159
|
+
next();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Serve pre-rendered HTML pages, applying Vite's transforms at serve time.
|
|
163
|
+
return () => {
|
|
164
|
+
server.middlewares.use(
|
|
165
|
+
async (req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => {
|
|
166
|
+
const url = req.originalUrl ?? req.url;
|
|
167
|
+
if (!url) return next();
|
|
168
|
+
|
|
169
|
+
const urlPath = url.split('?')[0]?.split('#')[0] ?? '/';
|
|
170
|
+
|
|
171
|
+
// Try cache first, fall back to on-demand render
|
|
172
|
+
let markup = pageCache.get(urlPath);
|
|
173
|
+
if (!markup) {
|
|
174
|
+
const htmlPath = resolveHtmlPath(url, root);
|
|
175
|
+
if (!htmlPath) return next();
|
|
176
|
+
try {
|
|
177
|
+
const result = generateStaticTemplate(htmlPath);
|
|
178
|
+
markup = result.markup;
|
|
179
|
+
result.cleanup();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`\x1b[31mError processing ${htmlPath}:\x1b[0m`, error);
|
|
182
|
+
return next(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Let Vite inject its HMR client and process module scripts
|
|
188
|
+
markup = await server.transformIndexHtml(url, markup);
|
|
189
|
+
|
|
190
|
+
res.statusCode = 200;
|
|
191
|
+
res.setHeader('Content-Type', 'text/html');
|
|
192
|
+
res.end(markup);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(`\x1b[31mError transforming HTML for ${url}:\x1b[0m`, error);
|
|
195
|
+
next(error);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
};
|