thunderous-server 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -1
- package/package.json +10 -5
- package/src/cli.ts +33 -3
- package/src/config.ts +7 -1
- package/src/dev.ts +30 -3
- package/src/generate.ts +59 -26
- package/src/meta.ts +1 -0
- package/src/vendorize.ts +23 -7
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Thunderous Server
|
|
2
2
|
|
|
3
3
|
> [!CAUTION]
|
|
4
|
-
> This project is experimental. It
|
|
4
|
+
> This project is experimental. It may not be suitable for production use at this time, as it is subject to bugs and breaking changes.
|
|
5
5
|
|
|
6
6
|
**Thunderous Server is a web framework and static generator designed to support and supplement _plain old HTML_.**
|
|
7
7
|
|
|
@@ -119,6 +119,7 @@ Exported values are available in <script expr> tags.
|
|
|
119
119
|
|
|
120
120
|
## TODO
|
|
121
121
|
|
|
122
|
+
- [ ] Add support for named slots in layouts
|
|
122
123
|
- [ ] Create (or find existing) extension that supports:
|
|
123
124
|
- [ ] TypeScript inside `<script>` tags
|
|
124
125
|
- [ ] IDE navigation in `<script server>` and `<script expr>` tags (go to definition, find references, etc.)
|
|
@@ -127,3 +128,8 @@ Exported values are available in <script expr> tags.
|
|
|
127
128
|
- [ ] Typechecking to ensure the default export in `<script server>` is `Record<PropertyKey, unknown>`
|
|
128
129
|
- [ ] OPTIONAL: Lint and autofix for trailing `;` in `<script expr>` tags
|
|
129
130
|
_e.g., `('hello';)` is an invalid expression, so `<script expr>'hello';</script>` is technically incorrect. That said, Thunderous Server does handle this currently by stripping it from the content before it's evaluated._
|
|
131
|
+
|
|
132
|
+
### Known Issues
|
|
133
|
+
|
|
134
|
+
- [ ] The dev server is not reliably hot-reloading changes
|
|
135
|
+
- [ ] The CSR views are not behaving as expected -- unclear if this is a bug in the server or the CSR package.
|
package/dist/index.cjs
CHANGED
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunderous-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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>",
|
|
@@ -30,9 +30,7 @@
|
|
|
30
30
|
"@types/node": "^24.9.1",
|
|
31
31
|
"@types/resolve": "^1.20.6",
|
|
32
32
|
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
|
33
|
-
"connect-livereload": "^0.6.1",
|
|
34
33
|
"eslint": "^9.38.0",
|
|
35
|
-
"livereload": "^0.10.3",
|
|
36
34
|
"prettier": "^3.6.2",
|
|
37
35
|
"serve": "^14.2.5",
|
|
38
36
|
"tsx": "^4.20.6",
|
|
@@ -41,13 +39,20 @@
|
|
|
41
39
|
"undici-types": "^7.16.0"
|
|
42
40
|
},
|
|
43
41
|
"dependencies": {
|
|
42
|
+
"chalk": "^5.6.2",
|
|
43
|
+
"connect-livereload": "^0.6.1",
|
|
44
|
+
"emoji-space-shim": "^0.1.7",
|
|
44
45
|
"es-module-lexer": "^1.7.0",
|
|
45
46
|
"express": "^5.1.0",
|
|
47
|
+
"livereload": "^0.10.3",
|
|
48
|
+
"node-emoji": "^2.2.0",
|
|
49
|
+
"nodemon": "^3.1.14",
|
|
46
50
|
"resolve": "^1.22.11",
|
|
47
|
-
"resolve.exports": "^2.0.3"
|
|
51
|
+
"resolve.exports": "^2.0.3",
|
|
52
|
+
"string-width": "^8.2.0"
|
|
48
53
|
},
|
|
49
54
|
"peerDependencies": {
|
|
50
|
-
"thunderous": ">=2.4.
|
|
55
|
+
"thunderous": ">=2.4.2"
|
|
51
56
|
},
|
|
52
57
|
"scripts": {
|
|
53
58
|
"build": "tsup --no-clean",
|
package/src/cli.ts
CHANGED
|
@@ -1,13 +1,43 @@
|
|
|
1
1
|
import { build } from './build';
|
|
2
|
-
import
|
|
2
|
+
import nodemon from 'nodemon';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { config } from './config';
|
|
5
|
+
import { relative, resolve } from 'path';
|
|
6
|
+
import chalk from 'chalk';
|
|
3
7
|
|
|
4
8
|
const args = process.argv.slice(2);
|
|
5
9
|
|
|
6
10
|
if (args[0] === 'dev') {
|
|
7
11
|
try {
|
|
8
|
-
|
|
12
|
+
const ignoreFile = readFileSync('.gitignore', 'utf-8');
|
|
13
|
+
const ignores = ignoreFile.split('\n').filter((line) => line.trim() !== '' && !line.startsWith('#'));
|
|
14
|
+
|
|
15
|
+
// Set up nodemon for auto-restart on server changes
|
|
16
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
17
|
+
nodemon({
|
|
18
|
+
script: resolve(`${import.meta.dirname}/dev.ts`),
|
|
19
|
+
ignore: ignores,
|
|
20
|
+
watch: [config.baseDir],
|
|
21
|
+
exec: 'tsx',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
nodemon
|
|
25
|
+
.on('start', () => {
|
|
26
|
+
// console.log('App has started');
|
|
27
|
+
})
|
|
28
|
+
.on('quit', () => {
|
|
29
|
+
console.log(chalk.green('\nServer shut down successfully.\n'));
|
|
30
|
+
process.exit();
|
|
31
|
+
})
|
|
32
|
+
.on('restart', (files) => {
|
|
33
|
+
const filesList = (files ?? ['(none)']).map(
|
|
34
|
+
(file) => ` • ${chalk.cyan.underline(relative(config.configDir ?? process.cwd(), file))}\n`,
|
|
35
|
+
);
|
|
36
|
+
console.log('\nUpdates detected:\n', filesList.join(''));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
9
39
|
} catch (error) {
|
|
10
|
-
console.error('
|
|
40
|
+
console.error('\x1b[31mFailed to start server:\x1b[0m', error);
|
|
11
41
|
process.exit(1);
|
|
12
42
|
}
|
|
13
43
|
} else if (args[0] === 'build') {
|
package/src/config.ts
CHANGED
|
@@ -19,12 +19,17 @@ export type ThunderousConfig = {
|
|
|
19
19
|
* The output directory for builds.
|
|
20
20
|
*/
|
|
21
21
|
outDir: string;
|
|
22
|
+
/**
|
|
23
|
+
* Non-configurable. The directory where the `thunderous.config.ts` file is located.
|
|
24
|
+
*/
|
|
25
|
+
readonly configDir: string | null;
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
const DEFAULT_CONFIG = Object.freeze({
|
|
25
29
|
name: 'Thunderous Project',
|
|
26
30
|
baseDir: 'src',
|
|
27
31
|
outDir: 'dist',
|
|
32
|
+
configDir: null,
|
|
28
33
|
});
|
|
29
34
|
|
|
30
35
|
/** Find and import the `thunderous.config.ts` file. */
|
|
@@ -41,12 +46,13 @@ const resolveConfig = (): ThunderousConfig => {
|
|
|
41
46
|
configDir = currentDir;
|
|
42
47
|
}
|
|
43
48
|
if (rootDir !== undefined && configDir !== undefined) {
|
|
44
|
-
const configPath = join(configDir
|
|
49
|
+
const configPath = join(configDir, 'thunderous.config.ts');
|
|
45
50
|
const configOverrides: Partial<ThunderousConfig> = require(configPath);
|
|
46
51
|
return {
|
|
47
52
|
name: configOverrides.name ?? DEFAULT_CONFIG.name,
|
|
48
53
|
baseDir: configOverrides.baseDir?.replace(/^\/*/, '') ?? DEFAULT_CONFIG.baseDir,
|
|
49
54
|
outDir: configOverrides.outDir ?? DEFAULT_CONFIG.outDir,
|
|
55
|
+
configDir,
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
const parentDir = resolve(currentDir, '..');
|
package/src/dev.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { readdirSync, statSync, mkdtempSync } from 'fs';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync, mkdtempSync } from 'fs';
|
|
3
3
|
import { join, relative, resolve } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
|
+
import { ModuleKind, ScriptTarget, transpileModule, ImportsNotUsedAsValues } from 'typescript';
|
|
5
6
|
import { bootstrapThunderous, generateStaticTemplate, generateImportMap, injectImportMap } from './generate';
|
|
6
7
|
import { config } from './config';
|
|
7
8
|
import livereload from 'livereload';
|
|
@@ -32,7 +33,7 @@ const bootstrapRoutes = (dir: string, app: express.Express, vendorDir: string) =
|
|
|
32
33
|
|
|
33
34
|
// Generate import map if there are client entry files
|
|
34
35
|
if (result.clientEntryFiles.length > 0) {
|
|
35
|
-
const importMapJson = generateImportMap(result.clientEntryFiles);
|
|
36
|
+
const importMapJson = generateImportMap(result.clientEntryFiles, vendorDir);
|
|
36
37
|
markup = injectImportMap(markup, importMapJson);
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -51,7 +52,7 @@ const bootstrapRoutes = (dir: string, app: express.Express, vendorDir: string) =
|
|
|
51
52
|
export const dev = () => {
|
|
52
53
|
console.log('\n\x1b[36m\x1b[1m⚡⚡ Starting development server... ⚡⚡\x1b[0m\x1b[0m\n');
|
|
53
54
|
|
|
54
|
-
const PORT = process.env
|
|
55
|
+
const PORT = process.env.PORT ?? 3000;
|
|
55
56
|
|
|
56
57
|
// Set up simple express server
|
|
57
58
|
const app = express();
|
|
@@ -81,6 +82,29 @@ export const dev = () => {
|
|
|
81
82
|
bootstrapThunderous();
|
|
82
83
|
bootstrapRoutes(`./${config.baseDir}`, app, vendorDir);
|
|
83
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
|
+
|
|
84
108
|
// Serve static assets from the base directory
|
|
85
109
|
app.use(express.static(config.baseDir));
|
|
86
110
|
|
|
@@ -111,3 +135,6 @@ export const dev = () => {
|
|
|
111
135
|
});
|
|
112
136
|
});
|
|
113
137
|
};
|
|
138
|
+
|
|
139
|
+
// start directly, since this file is targeted directly by nodemon
|
|
140
|
+
dev();
|
package/src/generate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join, relative, resolve } from 'path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { html } from 'thunderous';
|
|
@@ -17,6 +17,7 @@ const outRequire = createRequire(resolve(config.baseDir));
|
|
|
17
17
|
// track state outside the function so that we only have to set one `onServerDefine` handler
|
|
18
18
|
const renderState = {
|
|
19
19
|
markup: '',
|
|
20
|
+
insertedTags: new Set<string>(),
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
// ── Shared TypeScript compiler options (created once, reused everywhere) ──
|
|
@@ -43,6 +44,8 @@ export const bootstrapThunderous = () => {
|
|
|
43
44
|
const { insertTemplates, onServerDefine } = Thunderous;
|
|
44
45
|
// Update the markup each time a thunderous element is defined on the server
|
|
45
46
|
onServerDefine((tagName, innerHTML) => {
|
|
47
|
+
if (renderState.insertedTags.has(tagName)) return;
|
|
48
|
+
renderState.insertedTags.add(tagName);
|
|
46
49
|
renderState.markup = insertTemplates(
|
|
47
50
|
tagName,
|
|
48
51
|
innerHTML.replace(/\s+/gm, ' ').replace(/ >/g, '>'),
|
|
@@ -105,6 +108,13 @@ const extractTags = (markup: string) => {
|
|
|
105
108
|
continue;
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
// Skip HTML comments <!-- ... -->
|
|
112
|
+
if (markup.startsWith('<!--', i)) {
|
|
113
|
+
const commentEnd = markup.indexOf('-->', i + 4);
|
|
114
|
+
i = commentEnd === -1 ? markup.length : commentEnd + 3;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
// <?layout href="…">
|
|
109
119
|
if (markup.startsWith('<?layout', i)) {
|
|
110
120
|
const close = markup.indexOf('>', i);
|
|
@@ -203,6 +213,7 @@ const extractTags = (markup: string) => {
|
|
|
203
213
|
*/
|
|
204
214
|
export const generateStaticTemplate = (filePath: string) => {
|
|
205
215
|
renderState.markup = readFileSync(filePath, 'utf-8');
|
|
216
|
+
renderState.insertedTags.clear();
|
|
206
217
|
|
|
207
218
|
const name = basename(filePath, extname(filePath));
|
|
208
219
|
|
|
@@ -244,7 +255,8 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
244
255
|
// ── Extract and apply layouts ──
|
|
245
256
|
const { layouts } = extractTags(renderState.markup);
|
|
246
257
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
247
|
-
const l = layouts[i]
|
|
258
|
+
const l = layouts[i];
|
|
259
|
+
if (!l) continue;
|
|
248
260
|
renderState.markup = renderState.markup.slice(0, l.start) + renderState.markup.slice(l.end);
|
|
249
261
|
}
|
|
250
262
|
renderState.markup = renderState.markup.trim();
|
|
@@ -277,6 +289,8 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
277
289
|
const scriptKey = (s: ParsedScript) => `${s.kind}|${s.href ?? ''}|${s.content}`;
|
|
278
290
|
const replacementMap = new Map<string, string>();
|
|
279
291
|
|
|
292
|
+
mkdirSync(resolvedOutDir, { recursive: true });
|
|
293
|
+
|
|
280
294
|
let scriptIndex = 0;
|
|
281
295
|
for (const script of scripts) {
|
|
282
296
|
const key = scriptKey(script);
|
|
@@ -309,14 +323,14 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
309
323
|
delete outRequire.cache[outRequire.resolve(hrefAbsPath)];
|
|
310
324
|
module = outRequire(hrefAbsPath);
|
|
311
325
|
} else {
|
|
312
|
-
// inline: write temp .ts so
|
|
313
|
-
const tsScriptFile = join(
|
|
326
|
+
// inline: write temp .ts into baseDir so relative imports resolve correctly
|
|
327
|
+
const tsScriptFile = join(resolvedBaseDir, `${name}-${scriptIndex}.tmp.ts`);
|
|
314
328
|
writeFileSync(tsScriptFile, `// @ts-nocheck\n${content}`, 'utf-8');
|
|
315
329
|
tempFilesToCleanup.push(tsScriptFile);
|
|
316
330
|
delete outRequire.cache[outRequire.resolve(tsScriptFile)];
|
|
317
331
|
module = outRequire(tsScriptFile);
|
|
318
332
|
}
|
|
319
|
-
const values = module
|
|
333
|
+
const values = module.default ?? {};
|
|
320
334
|
for (const k in values) {
|
|
321
335
|
const val = (values as Record<string, unknown>)[k];
|
|
322
336
|
safeValues[k] = typeof val === 'string' ? escapeHtml(val) : val;
|
|
@@ -340,11 +354,14 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
340
354
|
const js = transpileTs(`// @ts-nocheck\n${content}`, `${name}-${scriptIndex}.ts`);
|
|
341
355
|
const fixedJs = js
|
|
342
356
|
.replace(/(import\s+.+?\s+from\s+['"](?:\.\.?\/|\/)[^'"]+?)\.tsx?(['"])/gm, '$1.js$2')
|
|
343
|
-
.replace(/(import\s+.+?\s+from\s+['"](?:\.\.?\/|\/)[^'"]+?)(?<!\.m?js)(['"])/gm, '$1.js$2')
|
|
357
|
+
.replace(/(import\s+.+?\s+from\s+['"](?:\.\.?\/|\/)[^'"]+?)(?<!\.m?js)(['"])/gm, '$1.js$2')
|
|
358
|
+
// Convert relative ./ imports to absolute / so they resolve correctly
|
|
359
|
+
// regardless of the page's URL depth (e.g. /about/contact)
|
|
360
|
+
.replace(/(import\s+.+?\s+from\s+['"])\.\//gm, '$1/');
|
|
344
361
|
replacementMap.set(key, `<script type="module">\n${fixedJs}\n</script>`);
|
|
345
362
|
|
|
346
|
-
// Write .js to
|
|
347
|
-
const jsOutPath = join(
|
|
363
|
+
// Write .js to baseDir for vendorization (so relative imports resolve from source tree)
|
|
364
|
+
const jsOutPath = join(resolvedBaseDir, `${name}-${scriptIndex}.tmp.js`);
|
|
348
365
|
writeFileSync(jsOutPath, js, 'utf-8');
|
|
349
366
|
tempFilesToCleanup.push(jsOutPath);
|
|
350
367
|
clientEntryFiles.push(resolve(jsOutPath));
|
|
@@ -356,7 +373,8 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
356
373
|
// ── Re-parse and apply all replacements in one reverse pass ──
|
|
357
374
|
const { scripts: freshScripts } = extractTags(renderState.markup);
|
|
358
375
|
for (let i = freshScripts.length - 1; i >= 0; i--) {
|
|
359
|
-
const script = freshScripts[i]
|
|
376
|
+
const script = freshScripts[i];
|
|
377
|
+
if (!script) continue;
|
|
360
378
|
const key = scriptKey(script);
|
|
361
379
|
const replacement = replacementMap.get(key);
|
|
362
380
|
if (replacement === undefined) continue;
|
|
@@ -403,16 +421,17 @@ export const generateStaticTemplate = (filePath: string) => {
|
|
|
403
421
|
* Generate import maps from entry files using vendorization.
|
|
404
422
|
* Returns the import map JSON string that can be injected into HTML.
|
|
405
423
|
*/
|
|
406
|
-
export const generateImportMap = (entryFiles: string[]): string => {
|
|
424
|
+
export const generateImportMap = (entryFiles: string[], outputDir?: string): string => {
|
|
425
|
+
const outDir = outputDir ?? config.outDir;
|
|
407
426
|
if (entryFiles.length === 0) {
|
|
408
427
|
return JSON.stringify({ imports: {} }, null, 2);
|
|
409
428
|
}
|
|
410
429
|
|
|
411
430
|
console.log(`\x1b[90mVendorizing dependencies from ${entryFiles.length} entry file(s)...\x1b[0m`);
|
|
412
|
-
processNodeModules(entryFiles,
|
|
431
|
+
processNodeModules(entryFiles, outDir);
|
|
413
432
|
console.log(`\x1b[32m✓ Import map generated\x1b[0m`);
|
|
414
433
|
|
|
415
|
-
const importMapPath = join(
|
|
434
|
+
const importMapPath = join(outDir, 'importmap.json');
|
|
416
435
|
if (existsSync(importMapPath)) {
|
|
417
436
|
return readFileSync(importMapPath, 'utf-8');
|
|
418
437
|
}
|
|
@@ -423,27 +442,41 @@ export const generateImportMap = (entryFiles: string[]): string => {
|
|
|
423
442
|
/**
|
|
424
443
|
* Inject an import map into HTML content.
|
|
425
444
|
* If an import map script already exists, it will be replaced.
|
|
426
|
-
* Otherwise, it will be injected
|
|
445
|
+
* Otherwise, it will be injected into the <head> so it's in a consistent
|
|
446
|
+
* position across all pages (important for CSR partial-update validation).
|
|
427
447
|
*/
|
|
428
448
|
export const injectImportMap = (html: string, importMapJson: string): string => {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
// Check if there's already an import map and replace it
|
|
432
|
-
const existingImportMapRegex = /<script\s+type="importmap"[^>]*>[\s\S]*?<\/script>/i;
|
|
433
|
-
if (existingImportMapRegex.test(html)) {
|
|
434
|
-
return html.replace(existingImportMapRegex, importMapTag);
|
|
435
|
-
}
|
|
449
|
+
const scriptId = `IMPORTMAP_${crypto.randomUUID()}`;
|
|
450
|
+
const importMapTag = `<script type="importmap" id="${scriptId}">\n${importMapJson}\n</script>`;
|
|
436
451
|
|
|
437
|
-
//
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
440
|
-
return html.
|
|
452
|
+
// Check if we've already injected an import map (by our ID pattern) and replace it
|
|
453
|
+
const ownImportMapRegex = /<script\s+type="importmap"\s+id="IMPORTMAP_[^"]*"[^>]*>[\s\S]*?<\/script>/i;
|
|
454
|
+
if (ownImportMapRegex.test(html)) {
|
|
455
|
+
return html.replace(ownImportMapRegex, importMapTag);
|
|
441
456
|
}
|
|
442
457
|
|
|
443
|
-
//
|
|
458
|
+
// Inject before </head> so the import map is always in a consistent position
|
|
444
459
|
const headCloseMatch = /<\/head>/i.exec(html);
|
|
445
460
|
if (headCloseMatch?.index !== undefined) {
|
|
446
|
-
return html.slice(0, headCloseMatch.index) + importMapTag + '\n' + html.slice(headCloseMatch.index);
|
|
461
|
+
return html.slice(0, headCloseMatch.index) + '\t' + importMapTag + '\n' + html.slice(headCloseMatch.index);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Fallback: inject before first script tag (skip HTML comments)
|
|
465
|
+
let searchPos = 0;
|
|
466
|
+
while (searchPos < html.length) {
|
|
467
|
+
// Skip HTML comments
|
|
468
|
+
const commentStart = html.indexOf('<!--', searchPos);
|
|
469
|
+
const scriptMatch = /<script/i.exec(html.slice(searchPos));
|
|
470
|
+
if (scriptMatch === null) break;
|
|
471
|
+
const scriptPos = searchPos + scriptMatch.index;
|
|
472
|
+
// If a comment starts before this <script, skip past the comment
|
|
473
|
+
if (commentStart !== -1 && commentStart < scriptPos) {
|
|
474
|
+
const commentEnd = html.indexOf('-->', commentStart + 4);
|
|
475
|
+
searchPos = commentEnd === -1 ? html.length : commentEnd + 3;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
// Found a real <script outside of a comment
|
|
479
|
+
return html.slice(0, scriptPos) + importMapTag + '\n' + html.slice(scriptPos);
|
|
447
480
|
}
|
|
448
481
|
|
|
449
482
|
// Last resort: add at the end
|
package/src/meta.ts
CHANGED
package/src/vendorize.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { accessSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { accessSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path, { dirname, relative, join } from 'node:path';
|
|
3
3
|
import resolve from 'resolve';
|
|
4
4
|
import { resolve as resolveExports } from 'resolve.exports';
|
|
@@ -31,7 +31,21 @@ export function processNodeModules(entryFiles: string[], outputDir?: string) {
|
|
|
31
31
|
if (seen.has(file)) continue;
|
|
32
32
|
seen.add(file);
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// In dev mode, .js may not exist yet; fall back to .ts source and transpile
|
|
35
|
+
let code: string;
|
|
36
|
+
if (existsSync(file)) {
|
|
37
|
+
code = readFileSync(file, 'utf8');
|
|
38
|
+
} else {
|
|
39
|
+
const tsFile = file.replace(/\.js$/, '.ts');
|
|
40
|
+
const tsxFile = file.replace(/\.js$/, '.tsx');
|
|
41
|
+
if (existsSync(tsFile)) {
|
|
42
|
+
code = transpileTsFast(readFileSync(tsFile, 'utf8'), tsFile);
|
|
43
|
+
} else if (existsSync(tsxFile)) {
|
|
44
|
+
code = transpileTsFast(readFileSync(tsxFile, 'utf8'), tsxFile);
|
|
45
|
+
} else {
|
|
46
|
+
throw new Error(`Cannot find module: ${file} (also tried .ts/.tsx)`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
35
49
|
const [imports] = parse(code);
|
|
36
50
|
|
|
37
51
|
for (const im of imports) {
|
|
@@ -49,12 +63,12 @@ export function processNodeModules(entryFiles: string[], outputDir?: string) {
|
|
|
49
63
|
// Prefer declared export for browser/import
|
|
50
64
|
const sub = resolveExports(pkg, '.', { conditions: CONDITIONS });
|
|
51
65
|
if (sub) {
|
|
52
|
-
pkg
|
|
66
|
+
pkg.main = sub;
|
|
53
67
|
return pkg;
|
|
54
68
|
}
|
|
55
69
|
// If no exports field, prefer "module" over "main" for ESM
|
|
56
|
-
if (pkg
|
|
57
|
-
pkg
|
|
70
|
+
if (pkg.module) {
|
|
71
|
+
pkg.main = pkg.module;
|
|
58
72
|
}
|
|
59
73
|
// Otherwise fall back to pkg.main (default behavior)
|
|
60
74
|
return pkg;
|
|
@@ -84,7 +98,9 @@ export function processNodeModules(entryFiles: string[], outputDir?: string) {
|
|
|
84
98
|
|
|
85
99
|
function withJsOrTs(p: string) {
|
|
86
100
|
const cand = [p, p + '.mjs', p + '.js', p + '.ts', p + '.tsx'];
|
|
87
|
-
for (const c of cand)
|
|
101
|
+
for (const c of cand) {
|
|
102
|
+
if (existsSync(c)) return c;
|
|
103
|
+
}
|
|
88
104
|
return p;
|
|
89
105
|
}
|
|
90
106
|
|
|
@@ -92,7 +108,7 @@ function vendorizeFile(absPath: string, vendorRoot: string) {
|
|
|
92
108
|
// Find package root & metadata
|
|
93
109
|
const pkgRoot = findPkgRoot(absPath);
|
|
94
110
|
const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')) as Record<string, string>;
|
|
95
|
-
const versionTag = `${pkg
|
|
111
|
+
const versionTag = `${pkg.name?.replace('/', '__') ?? 'unknown'}@${pkg.version ?? '0.0.0'}`;
|
|
96
112
|
const relFromPkg = relative(pkgRoot, absPath);
|
|
97
113
|
const outDir = join(vendorRoot, versionTag);
|
|
98
114
|
const outPath = join(outDir, relFromPkg.replace(/\.(ts|tsx)$/, '.js'));
|