thunderous-server 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/bin/thunderous +10 -0
- package/dist/index.cjs +69 -0
- package/dist/index.d.cts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +40 -0
- package/package.json +63 -0
- package/src/build.ts +134 -0
- package/src/cli.ts +24 -0
- package/src/config.ts +61 -0
- package/src/dev.ts +113 -0
- package/src/generate.ts +451 -0
- package/src/index.ts +2 -0
- package/src/meta.ts +80 -0
- package/src/utilities.ts +26 -0
- package/src/vendorize.ts +190 -0
- package/types/global.d.ts +1 -0
package/src/meta.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ThunderousConfig } from './config';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A breadcrumb (name and path) for a given page.
|
|
5
|
+
*/
|
|
6
|
+
export type Breadcrumb = {
|
|
7
|
+
/**
|
|
8
|
+
* The name of the page.
|
|
9
|
+
*/
|
|
10
|
+
name: string;
|
|
11
|
+
/**
|
|
12
|
+
* The full URL path to the page.
|
|
13
|
+
*/
|
|
14
|
+
pathname: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Metadata about the current page being rendered.
|
|
19
|
+
*/
|
|
20
|
+
type Meta = {
|
|
21
|
+
/**
|
|
22
|
+
* The configuration object from `thunderous.config.ts`.
|
|
23
|
+
*/
|
|
24
|
+
config: ThunderousConfig;
|
|
25
|
+
/**
|
|
26
|
+
* The pathname of the current page being rendered.
|
|
27
|
+
*/
|
|
28
|
+
pathname: string;
|
|
29
|
+
/**
|
|
30
|
+
* The breadcrumbs of the current page being rendered.
|
|
31
|
+
*/
|
|
32
|
+
breadcrumbs: Breadcrumb[];
|
|
33
|
+
/**
|
|
34
|
+
* The inferred title of the current page being rendered.
|
|
35
|
+
*
|
|
36
|
+
* This is derived from the filename, replacing hyphens and underscores
|
|
37
|
+
* with spaces, and capitalizing the first letter of each word.
|
|
38
|
+
*/
|
|
39
|
+
title: string;
|
|
40
|
+
/**
|
|
41
|
+
* The name of the current page being rendered.
|
|
42
|
+
*
|
|
43
|
+
* This is the filename without the extension.
|
|
44
|
+
*/
|
|
45
|
+
name: string;
|
|
46
|
+
/**
|
|
47
|
+
* The filename of the current page being rendered.
|
|
48
|
+
*
|
|
49
|
+
* This is the full filename including the extension.
|
|
50
|
+
*/
|
|
51
|
+
filename: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const metaState: Meta = {
|
|
55
|
+
config: {
|
|
56
|
+
name: '',
|
|
57
|
+
baseDir: '',
|
|
58
|
+
outDir: '',
|
|
59
|
+
},
|
|
60
|
+
pathname: '/',
|
|
61
|
+
breadcrumbs: [],
|
|
62
|
+
title: '',
|
|
63
|
+
name: '',
|
|
64
|
+
filename: '',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Set metadata context for the current page being rendered. */
|
|
68
|
+
export const setMeta = (meta: Partial<Meta>) => {
|
|
69
|
+
Object.assign(metaState, meta);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Get metadata about the current page being rendered. */
|
|
73
|
+
export const getMeta = (): Meta => {
|
|
74
|
+
// immutable copy prevents inadvertent mutations
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
...metaState,
|
|
77
|
+
config: { ...metaState.config },
|
|
78
|
+
breadcrumbs: [...metaState.breadcrumbs],
|
|
79
|
+
});
|
|
80
|
+
};
|
package/src/utilities.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape a string for safe insertion into HTML.
|
|
3
|
+
*/
|
|
4
|
+
export const escapeHtml = (str: string): string => {
|
|
5
|
+
return str
|
|
6
|
+
.replace(/&/g, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, ''');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class RawHtml {
|
|
14
|
+
value: string;
|
|
15
|
+
constructor(value: string) {
|
|
16
|
+
this.value = value;
|
|
17
|
+
}
|
|
18
|
+
toString() {
|
|
19
|
+
return this.value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a string so it bypasses automatic HTML escaping in `<script expr>`.
|
|
25
|
+
*/
|
|
26
|
+
export const raw = (str: string): RawHtml => new RawHtml(str);
|
package/src/vendorize.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { accessSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path, { dirname, relative, join } from 'node:path';
|
|
3
|
+
import resolve from 'resolve';
|
|
4
|
+
import { resolve as resolveExports } from 'resolve.exports';
|
|
5
|
+
import { parse, initSync } from 'es-module-lexer';
|
|
6
|
+
import { ImportsNotUsedAsValues, JsxEmit, ModuleKind, ScriptTarget, transpileModule } from 'typescript';
|
|
7
|
+
import { config } from './config';
|
|
8
|
+
initSync();
|
|
9
|
+
|
|
10
|
+
const CONDITIONS = ['browser', 'import', 'default'];
|
|
11
|
+
|
|
12
|
+
type Graph = Set<string>;
|
|
13
|
+
const isBare = (s: string) => !s.startsWith('.') && !s.startsWith('/') && !s.startsWith('http');
|
|
14
|
+
const isUrl = (s: string) => s.startsWith('http:') || s.startsWith('https:') || s.startsWith('data:');
|
|
15
|
+
const isNodeBuiltin = (s: string) => s.startsWith('node:');
|
|
16
|
+
|
|
17
|
+
export function processNodeModules(entryFiles: string[], outputDir?: string) {
|
|
18
|
+
const _outputDir = outputDir ?? config.outDir;
|
|
19
|
+
const vendorRoot = join(_outputDir, 'vendor');
|
|
20
|
+
const seen: Graph = new Set();
|
|
21
|
+
const q = [...entryFiles];
|
|
22
|
+
const importMap: Record<string, string> = {};
|
|
23
|
+
|
|
24
|
+
while (q.length) {
|
|
25
|
+
let file = q.pop()!;
|
|
26
|
+
// Ensure .js extension: replace .ts/.tsx with .js, or add .js if no extension
|
|
27
|
+
file = file.replace(/\.tsx?$/, '.js');
|
|
28
|
+
if (!file.endsWith('.js') && !file.endsWith('.mjs')) {
|
|
29
|
+
file = file + '.js';
|
|
30
|
+
}
|
|
31
|
+
if (seen.has(file)) continue;
|
|
32
|
+
seen.add(file);
|
|
33
|
+
|
|
34
|
+
const code = readFileSync(file, 'utf8');
|
|
35
|
+
const [imports] = parse(code);
|
|
36
|
+
|
|
37
|
+
for (const im of imports) {
|
|
38
|
+
const spec = code
|
|
39
|
+
.slice(im.s, im.e)
|
|
40
|
+
.trim()
|
|
41
|
+
.replace(/^['"]|['"]$/g, '');
|
|
42
|
+
if (spec === '' || isUrl(spec) || isNodeBuiltin(spec) || spec === 'import.meta') continue;
|
|
43
|
+
|
|
44
|
+
if (isBare(spec)) {
|
|
45
|
+
// 1) Resolve package entry honoring "exports" + conditions
|
|
46
|
+
const pkgEntry = resolve.sync(spec, {
|
|
47
|
+
basedir: dirname(file),
|
|
48
|
+
packageFilter(pkg) {
|
|
49
|
+
// Prefer declared export for browser/import
|
|
50
|
+
const sub = resolveExports(pkg, '.', { conditions: CONDITIONS });
|
|
51
|
+
if (sub) {
|
|
52
|
+
pkg['main'] = sub;
|
|
53
|
+
return pkg;
|
|
54
|
+
}
|
|
55
|
+
// If no exports field, prefer "module" over "main" for ESM
|
|
56
|
+
if (pkg['module']) {
|
|
57
|
+
pkg['main'] = pkg['module'];
|
|
58
|
+
}
|
|
59
|
+
// Otherwise fall back to pkg.main (default behavior)
|
|
60
|
+
return pkg;
|
|
61
|
+
},
|
|
62
|
+
extensions: ['.mjs', '.js', '.ts', '.tsx'], // allow TS in published packages
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 2) Copy/transform this package file into /dist/vendor/<name>@<version>/...
|
|
66
|
+
const mapped = vendorizeFile(pkgEntry, vendorRoot);
|
|
67
|
+
importMap[spec] = mapped.mappedUrl;
|
|
68
|
+
|
|
69
|
+
// 3) Enqueue its internal deps (we rewrite imports in vendor files to relative URLs)
|
|
70
|
+
q.push(...mapped.discoveredDeps.filter((f) => !seen.has(f)));
|
|
71
|
+
} else if (spec.startsWith('.') || spec.startsWith('/')) {
|
|
72
|
+
// local project module; include so we can walk transitive deps
|
|
73
|
+
const resolved = path.resolve(dirname(file), spec);
|
|
74
|
+
q.push(withJsOrTs(resolved));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
writeFileSync(join(_outputDir, 'importmap.json'), JSON.stringify({ imports: importMap }, null, 2));
|
|
80
|
+
// include es-module-shims in your HTML for Safari
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- helpers ---
|
|
84
|
+
|
|
85
|
+
function withJsOrTs(p: string) {
|
|
86
|
+
const cand = [p, p + '.mjs', p + '.js', p + '.ts', p + '.tsx'];
|
|
87
|
+
for (const c of cand) return c; // optimistic; upstream readFile will fail fast if wrong
|
|
88
|
+
return p;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function vendorizeFile(absPath: string, vendorRoot: string) {
|
|
92
|
+
// Find package root & metadata
|
|
93
|
+
const pkgRoot = findPkgRoot(absPath);
|
|
94
|
+
const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')) as Record<string, string>;
|
|
95
|
+
const versionTag = `${pkg['name']!.replace('/', '__')}@${pkg['version']!}`;
|
|
96
|
+
const relFromPkg = relative(pkgRoot, absPath);
|
|
97
|
+
const outDir = join(vendorRoot, versionTag);
|
|
98
|
+
const outPath = join(outDir, relFromPkg.replace(/\.(ts|tsx)$/, '.js'));
|
|
99
|
+
|
|
100
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Read & (if needed) transpile TS → JS (no typecheck, just emit)
|
|
103
|
+
const src = readFileSync(absPath, 'utf8');
|
|
104
|
+
const isTs = /\.(ts|tsx)$/.test(absPath);
|
|
105
|
+
const code = isTs ? transpileTsFast(src, absPath) : src;
|
|
106
|
+
|
|
107
|
+
// Rewrite its *internal* bare imports later; for package files we'll keep bare,
|
|
108
|
+
// because the import map will point top-level specifiers at the package entry.
|
|
109
|
+
const { rewritten, discoveredDeps } = rewriteAndDiscover(code, dirname(absPath), pkgRoot, outDir);
|
|
110
|
+
|
|
111
|
+
writeFileSync(outPath, rewritten, 'utf8');
|
|
112
|
+
|
|
113
|
+
// Get the vendorRoot's parent directory to make path relative from output directory
|
|
114
|
+
const outputDir = dirname(vendorRoot);
|
|
115
|
+
return {
|
|
116
|
+
mappedUrl: `/${relative(outputDir, outPath).replace(/\\/g, '/')}`,
|
|
117
|
+
discoveredDeps,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function findPkgRoot(start: string) {
|
|
122
|
+
let cur = start;
|
|
123
|
+
while (true) {
|
|
124
|
+
const candidate = join(cur, 'package.json');
|
|
125
|
+
try {
|
|
126
|
+
accessSync(candidate);
|
|
127
|
+
return cur;
|
|
128
|
+
} catch {
|
|
129
|
+
// No package.json found in this directory
|
|
130
|
+
}
|
|
131
|
+
const up = dirname(cur);
|
|
132
|
+
if (up === cur) throw new Error(`No package.json found for ${start}`);
|
|
133
|
+
cur = up;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function rewriteAndDiscover(code: string, basedir: string, pkgRoot: string, outDir: string) {
|
|
138
|
+
const [imports] = parse(code);
|
|
139
|
+
let offset = 0;
|
|
140
|
+
let rewritten = code;
|
|
141
|
+
const deps: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const im of imports) {
|
|
144
|
+
const s = im.s + offset,
|
|
145
|
+
e = im.e + offset;
|
|
146
|
+
const spec = rewritten
|
|
147
|
+
.slice(s, e)
|
|
148
|
+
.replace(/^['"]|['"]$/g, '')
|
|
149
|
+
.trim();
|
|
150
|
+
|
|
151
|
+
if (spec === '' || isUrl(spec) || isNodeBuiltin(spec)) continue;
|
|
152
|
+
|
|
153
|
+
if (isBare(spec)) {
|
|
154
|
+
// leave bare; import map will handle it
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (spec.startsWith('.') || spec.startsWith('/')) {
|
|
158
|
+
const abs = path.resolve(basedir, spec);
|
|
159
|
+
const targetRel = relative(pkgRoot, abs).replace(/\.(ts|tsx)$/, '.js');
|
|
160
|
+
const newSpec = path
|
|
161
|
+
.relative(join(outDir, relative(pkgRoot, basedir)), join(outDir, targetRel))
|
|
162
|
+
.replace(/\\/g, '/');
|
|
163
|
+
// patch string literal
|
|
164
|
+
const quoted = JSON.stringify(newSpec.startsWith('.') ? newSpec : './' + newSpec);
|
|
165
|
+
rewritten = rewritten.slice(0, s) + quoted + rewritten.slice(e);
|
|
166
|
+
offset += quoted.length - (e - s);
|
|
167
|
+
deps.push(withJsOrTs(abs));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { rewritten, discoveredDeps: deps };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// minimal TS transpile (no type-check), stays "buildless"
|
|
175
|
+
function transpileTsFast(source: string, fileName: string) {
|
|
176
|
+
const out = transpileModule(source, {
|
|
177
|
+
compilerOptions: {
|
|
178
|
+
module: ModuleKind.ESNext,
|
|
179
|
+
target: ScriptTarget.ES2020,
|
|
180
|
+
jsx: JsxEmit.ReactJSX,
|
|
181
|
+
sourceMap: false,
|
|
182
|
+
importsNotUsedAsValues: ImportsNotUsedAsValues.Remove,
|
|
183
|
+
verbatimModuleSyntax: true,
|
|
184
|
+
isolatedModules: true,
|
|
185
|
+
},
|
|
186
|
+
fileName,
|
|
187
|
+
reportDiagnostics: false,
|
|
188
|
+
});
|
|
189
|
+
return out.outputText;
|
|
190
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@total-typescript/ts-reset';
|