@stati/core 1.13.0 → 1.15.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/dist/constants.d.ts +8 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -4
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +15 -30
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +18 -12
- package/dist/core/utils/bundle-matching.utils.d.ts +99 -0
- package/dist/core/utils/bundle-matching.utils.d.ts.map +1 -0
- package/dist/core/utils/bundle-matching.utils.js +138 -0
- package/dist/core/utils/index.d.ts +4 -2
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +3 -1
- package/dist/core/utils/typescript.utils.d.ts +51 -29
- package/dist/core/utils/typescript.utils.d.ts.map +1 -1
- package/dist/core/utils/typescript.utils.js +230 -106
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types/config.d.ts +83 -17
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +11 -5
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript compilation utilities using esbuild.
|
|
3
3
|
* Provides functions for compiling TypeScript files and watching for changes.
|
|
4
|
+
* Supports multiple bundles with per-page targeting.
|
|
4
5
|
* @module core/utils/typescript
|
|
5
6
|
*/
|
|
6
7
|
import * as esbuild from 'esbuild';
|
|
8
|
+
import { type CompiledBundleInfo } from './bundle-matching.utils.js';
|
|
7
9
|
import type { TypeScriptConfig } from '../../types/config.js';
|
|
8
10
|
import type { Logger } from '../../types/logging.js';
|
|
9
11
|
/**
|
|
@@ -21,61 +23,69 @@ export interface CompileOptions {
|
|
|
21
23
|
/** Logger instance for output */
|
|
22
24
|
logger: Logger;
|
|
23
25
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Result of TypeScript compilation.
|
|
26
|
-
*/
|
|
27
|
-
export interface CompileResult {
|
|
28
|
-
/** The generated bundle filename (e.g., 'bundle-a1b2c3d4.js'), or undefined if compilation was skipped */
|
|
29
|
-
bundleFilename?: string;
|
|
30
|
-
}
|
|
31
26
|
/**
|
|
32
27
|
* Options for TypeScript file watcher.
|
|
28
|
+
* Note: Watcher is always in development mode (no hash, no minify, with sourcemaps).
|
|
33
29
|
*/
|
|
34
|
-
export interface WatchOptions
|
|
35
|
-
/**
|
|
36
|
-
|
|
30
|
+
export interface WatchOptions {
|
|
31
|
+
/** Root directory of the project */
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
/** TypeScript configuration */
|
|
34
|
+
config: TypeScriptConfig;
|
|
35
|
+
/** Output directory (defaults to 'dist') */
|
|
36
|
+
outDir?: string;
|
|
37
|
+
/** Logger instance for output */
|
|
38
|
+
logger: Logger;
|
|
39
|
+
/** Callback invoked when files are recompiled, receives results and compile time in ms */
|
|
40
|
+
onRebuild: (results: CompiledBundleInfo[], compileTimeMs: number) => void;
|
|
37
41
|
}
|
|
38
42
|
/**
|
|
39
43
|
* Compile TypeScript files using esbuild.
|
|
40
|
-
*
|
|
44
|
+
* Supports multiple bundles with parallel compilation.
|
|
41
45
|
*
|
|
42
46
|
* @param options - Compilation options
|
|
43
|
-
* @returns
|
|
47
|
+
* @returns Array of compilation results for each bundle
|
|
44
48
|
*
|
|
45
49
|
* @example
|
|
46
50
|
* ```typescript
|
|
47
|
-
* const
|
|
51
|
+
* const results = await compileTypeScript({
|
|
48
52
|
* projectRoot: process.cwd(),
|
|
49
|
-
* config: {
|
|
53
|
+
* config: {
|
|
54
|
+
* enabled: true,
|
|
55
|
+
* bundles: [
|
|
56
|
+
* { entryPoint: 'core.ts', bundleName: 'core' },
|
|
57
|
+
* { entryPoint: 'docs.ts', bundleName: 'docs', include: ['/docs/**'] }
|
|
58
|
+
* ]
|
|
59
|
+
* },
|
|
50
60
|
* mode: 'production',
|
|
51
61
|
* logger: console,
|
|
52
62
|
* });
|
|
53
|
-
* console.log(
|
|
63
|
+
* console.log(results[0].bundlePath); // '/_assets/core-a1b2c3d4.js'
|
|
54
64
|
* ```
|
|
55
65
|
*/
|
|
56
|
-
export declare function compileTypeScript(options: CompileOptions): Promise<
|
|
66
|
+
export declare function compileTypeScript(options: CompileOptions): Promise<CompiledBundleInfo[]>;
|
|
57
67
|
/**
|
|
58
68
|
* Create an esbuild watch context for development.
|
|
59
69
|
* Watches for TypeScript file changes and recompiles automatically.
|
|
70
|
+
* Supports multiple bundles with selective recompilation.
|
|
60
71
|
*
|
|
61
72
|
* @param options - Watch options including rebuild callback
|
|
62
|
-
* @returns
|
|
73
|
+
* @returns Array of esbuild build contexts for cleanup
|
|
63
74
|
*
|
|
64
75
|
* @example
|
|
65
76
|
* ```typescript
|
|
66
|
-
* const
|
|
77
|
+
* const watchers = await createTypeScriptWatcher({
|
|
67
78
|
* projectRoot: process.cwd(),
|
|
68
79
|
* config: { enabled: true },
|
|
69
|
-
* mode: 'development',
|
|
70
80
|
* logger: console,
|
|
71
|
-
* onRebuild: () => console.log(
|
|
81
|
+
* onRebuild: (results, compileTimeMs) => console.log(`Rebuilt ${results.length} bundles in ${compileTimeMs}ms`),
|
|
72
82
|
* });
|
|
73
83
|
*
|
|
74
84
|
* // Later, cleanup:
|
|
75
|
-
* await
|
|
85
|
+
* await Promise.all(watchers.map(w => w.dispose()));
|
|
76
86
|
* ```
|
|
77
87
|
*/
|
|
78
|
-
export declare function createTypeScriptWatcher(options: WatchOptions): Promise<esbuild.BuildContext>;
|
|
88
|
+
export declare function createTypeScriptWatcher(options: WatchOptions): Promise<esbuild.BuildContext[]>;
|
|
79
89
|
/**
|
|
80
90
|
* Compile stati.config.ts to a temporary JS file for import.
|
|
81
91
|
* Returns the path to the compiled file.
|
|
@@ -98,20 +108,32 @@ export declare function compileStatiConfig(configPath: string): Promise<string>;
|
|
|
98
108
|
*/
|
|
99
109
|
export declare function cleanupCompiledConfig(compiledPath: string): Promise<void>;
|
|
100
110
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
111
|
+
* Validates a bundle path for safety against XSS attacks.
|
|
112
|
+
* Only allows safe ASCII characters: alphanumeric, hyphens, underscores, dots, and forward slashes.
|
|
113
|
+
* Must start with / and end with .js, no encoded characters or unicode allowed.
|
|
114
|
+
*
|
|
115
|
+
* @param bundlePath - The bundle path to validate
|
|
116
|
+
* @returns true if the path is safe, false otherwise
|
|
117
|
+
*
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
export declare function isValidBundlePath(bundlePath: string): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Auto-inject TypeScript bundle script tags into HTML before </body>.
|
|
123
|
+
* Similar to SEO auto-injection, this adds script tags automatically
|
|
103
124
|
* so users don't need to modify their templates.
|
|
125
|
+
* Supports multiple bundles - injects all matched bundles in order.
|
|
104
126
|
*
|
|
105
127
|
* @param html - Rendered HTML content
|
|
106
|
-
* @param
|
|
107
|
-
* @returns HTML with injected script
|
|
128
|
+
* @param bundlePaths - Array of paths to compiled bundles (e.g., ['/_assets/core.js', '/_assets/docs.js'])
|
|
129
|
+
* @returns HTML with injected script tags
|
|
108
130
|
*
|
|
109
131
|
* @example
|
|
110
132
|
* ```typescript
|
|
111
133
|
* const html = '<html><body>Content</body></html>';
|
|
112
|
-
* const enhanced =
|
|
113
|
-
* // Returns
|
|
134
|
+
* const enhanced = autoInjectBundles(html, ['/_assets/core.js', '/_assets/docs.js']);
|
|
135
|
+
* // Returns HTML with both script tags injected before </body>
|
|
114
136
|
* ```
|
|
115
137
|
*/
|
|
116
|
-
export declare function
|
|
138
|
+
export declare function autoInjectBundles(html: string, bundlePaths: string[]): string;
|
|
117
139
|
//# sourceMappingURL=typescript.utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typescript.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/typescript.utils.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"typescript.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/typescript.utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAInC,OAAO,EAA6B,KAAK,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,uBAAuB,CAAC;AAC5E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAQrD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,IAAI,EAAE,aAAa,GAAG,YAAY,CAAC;IACnC,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,SAAS,EAAE,CAAC,OAAO,EAAE,kBAAkB,EAAE,EAAE,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3E;AAmGD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAuC9F;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAqFjC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB5E;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/E;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAqC7D;AAiBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CA8B7E"}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript compilation utilities using esbuild.
|
|
3
3
|
* Provides functions for compiling TypeScript files and watching for changes.
|
|
4
|
+
* Supports multiple bundles with per-page targeting.
|
|
4
5
|
* @module core/utils/typescript
|
|
5
6
|
*/
|
|
6
7
|
import * as esbuild from 'esbuild';
|
|
7
8
|
import * as path from 'node:path';
|
|
8
9
|
import * as fs from 'node:fs/promises';
|
|
9
10
|
import { pathExists } from './fs.utils.js';
|
|
10
|
-
import {
|
|
11
|
+
import { validateUniqueBundleNames } from './bundle-matching.utils.js';
|
|
12
|
+
import { DEFAULT_TS_SRC_DIR, DEFAULT_TS_OUT_DIR, DEFAULT_BUNDLES, DEFAULT_OUT_DIR, } from '../../constants.js';
|
|
13
|
+
/**
|
|
14
|
+
* Resolves TypeScript config with defaults based on mode.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
11
17
|
function resolveConfig(config, mode) {
|
|
12
18
|
const isProduction = mode === 'production';
|
|
13
19
|
return {
|
|
14
20
|
enabled: config.enabled,
|
|
15
21
|
srcDir: config.srcDir ?? DEFAULT_TS_SRC_DIR,
|
|
16
22
|
outDir: config.outDir ?? DEFAULT_TS_OUT_DIR,
|
|
17
|
-
|
|
18
|
-
bundleName: config.bundleName ?? DEFAULT_TS_BUNDLE_NAME,
|
|
23
|
+
bundles: config.bundles ?? [...DEFAULT_BUNDLES],
|
|
19
24
|
// hash/minify: always false in dev (config ignored), configurable in prod (default true)
|
|
20
25
|
hash: isProduction && (config.hash ?? true),
|
|
21
26
|
minify: isProduction && (config.minify ?? true),
|
|
@@ -24,130 +29,196 @@ function resolveConfig(config, mode) {
|
|
|
24
29
|
};
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
|
-
* Compile
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* @param options - Compilation options
|
|
31
|
-
* @returns The compilation result with bundle filename
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* const result = await compileTypeScript({
|
|
36
|
-
* projectRoot: process.cwd(),
|
|
37
|
-
* config: { enabled: true },
|
|
38
|
-
* mode: 'production',
|
|
39
|
-
* logger: console,
|
|
40
|
-
* });
|
|
41
|
-
* console.log(result.bundleFilename); // 'bundle-a1b2c3d4.js'
|
|
42
|
-
* ```
|
|
32
|
+
* Compile a single bundle using esbuild.
|
|
33
|
+
* @internal
|
|
43
34
|
*/
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const entryPath = path.join(projectRoot, resolved.srcDir, resolved.entryPoint);
|
|
48
|
-
// Output to configured build output directory (default: dist)
|
|
49
|
-
const outDir = path.join(projectRoot, globalOutDir || 'dist', resolved.outDir);
|
|
35
|
+
async function compileSingleBundle(bundleConfig, resolvedConfig, projectRoot, globalOutDir, logger) {
|
|
36
|
+
const entryPath = path.join(projectRoot, resolvedConfig.srcDir, bundleConfig.entryPoint);
|
|
37
|
+
const outDir = path.join(projectRoot, globalOutDir, resolvedConfig.outDir);
|
|
50
38
|
// Validate entry point exists
|
|
51
39
|
if (!(await pathExists(entryPath))) {
|
|
52
|
-
logger.warning(`TypeScript entry point not found: ${entryPath}`);
|
|
53
|
-
|
|
54
|
-
return {};
|
|
40
|
+
logger.warning(`TypeScript entry point not found: ${entryPath} (bundle: ${bundleConfig.bundleName})`);
|
|
41
|
+
return null;
|
|
55
42
|
}
|
|
56
|
-
logger.info('Compiling TypeScript...');
|
|
57
43
|
try {
|
|
58
44
|
const result = await esbuild.build({
|
|
59
45
|
entryPoints: [entryPath],
|
|
60
46
|
bundle: true,
|
|
61
47
|
outdir: outDir,
|
|
62
|
-
entryNames:
|
|
63
|
-
|
|
64
|
-
|
|
48
|
+
entryNames: resolvedConfig.hash
|
|
49
|
+
? `${bundleConfig.bundleName}-[hash]`
|
|
50
|
+
: bundleConfig.bundleName,
|
|
51
|
+
minify: resolvedConfig.minify,
|
|
52
|
+
sourcemap: resolvedConfig.sourceMaps,
|
|
65
53
|
target: 'es2022',
|
|
66
54
|
format: 'esm',
|
|
67
55
|
platform: 'browser',
|
|
68
56
|
logLevel: 'silent',
|
|
69
57
|
metafile: true,
|
|
70
58
|
});
|
|
71
|
-
// Extract the generated filename for
|
|
59
|
+
// Extract the generated filename for this bundle
|
|
72
60
|
const outputs = Object.keys(result.metafile?.outputs ?? {});
|
|
73
61
|
const bundleFile = outputs.find((f) => f.endsWith('.js'));
|
|
74
|
-
const bundleFilename = bundleFile ? path.basename(bundleFile) : `${
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
const bundleFilename = bundleFile ? path.basename(bundleFile) : `${bundleConfig.bundleName}.js`;
|
|
63
|
+
// Construct the path relative to site root
|
|
64
|
+
const bundlePath = path.posix.join('/', resolvedConfig.outDir, bundleFilename);
|
|
65
|
+
return {
|
|
66
|
+
config: bundleConfig,
|
|
67
|
+
filename: bundleFilename,
|
|
68
|
+
path: bundlePath,
|
|
69
|
+
};
|
|
77
70
|
}
|
|
78
71
|
catch (error) {
|
|
79
72
|
if (error instanceof Error) {
|
|
80
|
-
logger.error(`
|
|
73
|
+
logger.error(`Bundle '${bundleConfig.bundleName}' compilation failed: ${error.message}`);
|
|
81
74
|
}
|
|
82
75
|
throw error;
|
|
83
76
|
}
|
|
84
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Compile TypeScript files using esbuild.
|
|
80
|
+
* Supports multiple bundles with parallel compilation.
|
|
81
|
+
*
|
|
82
|
+
* @param options - Compilation options
|
|
83
|
+
* @returns Array of compilation results for each bundle
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const results = await compileTypeScript({
|
|
88
|
+
* projectRoot: process.cwd(),
|
|
89
|
+
* config: {
|
|
90
|
+
* enabled: true,
|
|
91
|
+
* bundles: [
|
|
92
|
+
* { entryPoint: 'core.ts', bundleName: 'core' },
|
|
93
|
+
* { entryPoint: 'docs.ts', bundleName: 'docs', include: ['/docs/**'] }
|
|
94
|
+
* ]
|
|
95
|
+
* },
|
|
96
|
+
* mode: 'production',
|
|
97
|
+
* logger: console,
|
|
98
|
+
* });
|
|
99
|
+
* console.log(results[0].bundlePath); // '/_assets/core-a1b2c3d4.js'
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export async function compileTypeScript(options) {
|
|
103
|
+
const { projectRoot, config, mode, logger, outDir: globalOutDir } = options;
|
|
104
|
+
const resolved = resolveConfig(config, mode);
|
|
105
|
+
const outputDir = globalOutDir || DEFAULT_OUT_DIR;
|
|
106
|
+
// Handle empty bundles array
|
|
107
|
+
if (resolved.bundles.length === 0) {
|
|
108
|
+
logger.warning('TypeScript is enabled but no bundles are configured. Add bundles to your stati.config.ts or disable TypeScript.');
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
// Validate unique bundle names early - fail fast with clear error message
|
|
112
|
+
validateUniqueBundleNames(resolved.bundles);
|
|
113
|
+
logger.info('');
|
|
114
|
+
logger.info(`Compiling TypeScript (${resolved.bundles.length} bundle${resolved.bundles.length > 1 ? 's' : ''})...`);
|
|
115
|
+
// Compile all bundles in parallel
|
|
116
|
+
const compilationPromises = resolved.bundles.map((bundleConfig) => compileSingleBundle(bundleConfig, resolved, projectRoot, outputDir, logger));
|
|
117
|
+
const results = await Promise.all(compilationPromises);
|
|
118
|
+
// Filter out null results (skipped bundles) and collect successful ones
|
|
119
|
+
const successfulResults = results.filter((r) => r !== null);
|
|
120
|
+
if (successfulResults.length > 0) {
|
|
121
|
+
const bundleNames = successfulResults.map((r) => r.filename).join(', ');
|
|
122
|
+
logger.success(`TypeScript compiled: ${bundleNames}`);
|
|
123
|
+
}
|
|
124
|
+
else if (resolved.bundles.length > 0) {
|
|
125
|
+
logger.warning('No TypeScript bundles were compiled (all entry points missing).');
|
|
126
|
+
}
|
|
127
|
+
return successfulResults;
|
|
128
|
+
}
|
|
85
129
|
/**
|
|
86
130
|
* Create an esbuild watch context for development.
|
|
87
131
|
* Watches for TypeScript file changes and recompiles automatically.
|
|
132
|
+
* Supports multiple bundles with selective recompilation.
|
|
88
133
|
*
|
|
89
134
|
* @param options - Watch options including rebuild callback
|
|
90
|
-
* @returns
|
|
135
|
+
* @returns Array of esbuild build contexts for cleanup
|
|
91
136
|
*
|
|
92
137
|
* @example
|
|
93
138
|
* ```typescript
|
|
94
|
-
* const
|
|
139
|
+
* const watchers = await createTypeScriptWatcher({
|
|
95
140
|
* projectRoot: process.cwd(),
|
|
96
141
|
* config: { enabled: true },
|
|
97
|
-
* mode: 'development',
|
|
98
142
|
* logger: console,
|
|
99
|
-
* onRebuild: () => console.log(
|
|
143
|
+
* onRebuild: (results, compileTimeMs) => console.log(`Rebuilt ${results.length} bundles in ${compileTimeMs}ms`),
|
|
100
144
|
* });
|
|
101
145
|
*
|
|
102
146
|
* // Later, cleanup:
|
|
103
|
-
* await
|
|
147
|
+
* await Promise.all(watchers.map(w => w.dispose()));
|
|
104
148
|
* ```
|
|
105
149
|
*/
|
|
106
150
|
export async function createTypeScriptWatcher(options) {
|
|
107
|
-
const { projectRoot, config,
|
|
108
|
-
const resolved = resolveConfig(config,
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
151
|
+
const { projectRoot, config, logger, onRebuild, outDir: globalOutDir } = options;
|
|
152
|
+
const resolved = resolveConfig(config, 'development');
|
|
153
|
+
const outputDir = globalOutDir || DEFAULT_OUT_DIR;
|
|
154
|
+
const outDir = path.join(projectRoot, outputDir, resolved.outDir);
|
|
155
|
+
const contexts = [];
|
|
156
|
+
const latestResults = new Map();
|
|
157
|
+
for (const bundleConfig of resolved.bundles) {
|
|
158
|
+
const entryPath = path.join(projectRoot, resolved.srcDir, bundleConfig.entryPoint);
|
|
159
|
+
// Validate entry point exists
|
|
160
|
+
if (!(await pathExists(entryPath))) {
|
|
161
|
+
logger.warning(`TypeScript entry point not found: ${entryPath} (bundle: ${bundleConfig.bundleName})`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const context = await esbuild.context({
|
|
165
|
+
entryPoints: [entryPath],
|
|
166
|
+
bundle: true,
|
|
167
|
+
outdir: outDir,
|
|
168
|
+
entryNames: bundleConfig.bundleName, // Stable filename in dev mode (no hash)
|
|
169
|
+
minify: false, // Dev mode: never minify for fast rebuilds
|
|
170
|
+
sourcemap: true, // Dev mode: always enable for debugging
|
|
171
|
+
target: 'es2022',
|
|
172
|
+
format: 'esm',
|
|
173
|
+
platform: 'browser',
|
|
174
|
+
logLevel: 'silent',
|
|
175
|
+
metafile: true,
|
|
176
|
+
plugins: [
|
|
177
|
+
{
|
|
178
|
+
name: 'stati-rebuild-notify',
|
|
179
|
+
setup(build) {
|
|
180
|
+
let startTime;
|
|
181
|
+
build.onStart(() => {
|
|
182
|
+
startTime = Date.now();
|
|
183
|
+
});
|
|
184
|
+
build.onEnd((result) => {
|
|
185
|
+
const compileTime = Date.now() - startTime;
|
|
186
|
+
if (result.errors.length > 0) {
|
|
187
|
+
result.errors.forEach((err) => {
|
|
188
|
+
logger.error(`TypeScript error in '${bundleConfig.bundleName}': ${err.text}`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Extract the generated filename
|
|
193
|
+
const outputs = Object.keys(result.metafile?.outputs ?? {});
|
|
194
|
+
const bundleFile = outputs.find((f) => f.endsWith('.js'));
|
|
195
|
+
const bundleFilename = bundleFile
|
|
196
|
+
? path.basename(bundleFile)
|
|
197
|
+
: `${bundleConfig.bundleName}.js`;
|
|
198
|
+
const bundlePath = path.posix.join('/', resolved.outDir, bundleFilename);
|
|
199
|
+
const bundleResult = {
|
|
200
|
+
config: bundleConfig,
|
|
201
|
+
filename: bundleFilename,
|
|
202
|
+
path: bundlePath,
|
|
203
|
+
};
|
|
204
|
+
latestResults.set(bundleConfig.bundleName, bundleResult);
|
|
205
|
+
logger.info(`TypeScript '${bundleConfig.bundleName}' recompiled.`);
|
|
206
|
+
// Notify with all current results and compile time
|
|
207
|
+
onRebuild(Array.from(latestResults.values()), compileTime);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
},
|
|
143
211
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
// Start watching
|
|
215
|
+
await context.watch();
|
|
216
|
+
contexts.push(context);
|
|
217
|
+
}
|
|
218
|
+
if (contexts.length > 0) {
|
|
219
|
+
logger.info(`Watching TypeScript files in ${resolved.srcDir}/ (${contexts.length} bundle${contexts.length > 1 ? 's' : ''})`);
|
|
220
|
+
}
|
|
221
|
+
return contexts;
|
|
151
222
|
}
|
|
152
223
|
/**
|
|
153
224
|
* Compile stati.config.ts to a temporary JS file for import.
|
|
@@ -192,35 +263,86 @@ export async function cleanupCompiledConfig(compiledPath) {
|
|
|
192
263
|
}
|
|
193
264
|
}
|
|
194
265
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
266
|
+
* Validates a bundle path for safety against XSS attacks.
|
|
267
|
+
* Only allows safe ASCII characters: alphanumeric, hyphens, underscores, dots, and forward slashes.
|
|
268
|
+
* Must start with / and end with .js, no encoded characters or unicode allowed.
|
|
269
|
+
*
|
|
270
|
+
* @param bundlePath - The bundle path to validate
|
|
271
|
+
* @returns true if the path is safe, false otherwise
|
|
272
|
+
*
|
|
273
|
+
* @internal
|
|
274
|
+
*/
|
|
275
|
+
export function isValidBundlePath(bundlePath) {
|
|
276
|
+
// Reject non-string or empty paths
|
|
277
|
+
if (typeof bundlePath !== 'string' || bundlePath.length === 0) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
// Reject paths with null bytes (can bypass security checks)
|
|
281
|
+
if (bundlePath.includes('\0')) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
// Reject paths with control characters or non-ASCII characters
|
|
285
|
+
// Check that all characters are in printable ASCII range (32-126)
|
|
286
|
+
for (let i = 0; i < bundlePath.length; i++) {
|
|
287
|
+
const charCode = bundlePath.charCodeAt(i);
|
|
288
|
+
if (charCode < 32 || charCode > 126) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Reject URL-encoded characters (e.g., %20, %3C) and unicode escapes (e.g., \u003C)
|
|
293
|
+
if (/%[0-9a-fA-F]{2}/.test(bundlePath) || /\\u[0-9a-fA-F]{4}/.test(bundlePath)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
// Reject HTML entities (e.g., < < <)
|
|
297
|
+
if (/&#?[a-zA-Z0-9]+;/.test(bundlePath)) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
// Reject path traversal attempts
|
|
301
|
+
if (bundlePath.includes('..') || bundlePath.includes('//')) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
// Validate against strict safe pattern
|
|
305
|
+
const safePathPattern = /^\/[a-zA-Z0-9_\-./]+\.js$/;
|
|
306
|
+
return safePathPattern.test(bundlePath);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Checks if a bundle script tag is already present in the HTML.
|
|
310
|
+
*
|
|
311
|
+
* @param html - The HTML content to check
|
|
312
|
+
* @param bundlePath - The bundle path to look for
|
|
313
|
+
* @returns true if the bundle is already included, false otherwise
|
|
314
|
+
*
|
|
315
|
+
* @internal
|
|
316
|
+
*/
|
|
317
|
+
function isBundleAlreadyIncluded(html, bundlePath) {
|
|
318
|
+
const escapedPath = bundlePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
319
|
+
const scriptTagPattern = new RegExp(`<script[^>]*\\ssrc=["']${escapedPath}["'][^>]*>`, 'i');
|
|
320
|
+
return scriptTagPattern.test(html);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Auto-inject TypeScript bundle script tags into HTML before </body>.
|
|
324
|
+
* Similar to SEO auto-injection, this adds script tags automatically
|
|
197
325
|
* so users don't need to modify their templates.
|
|
326
|
+
* Supports multiple bundles - injects all matched bundles in order.
|
|
198
327
|
*
|
|
199
328
|
* @param html - Rendered HTML content
|
|
200
|
-
* @param
|
|
201
|
-
* @returns HTML with injected script
|
|
329
|
+
* @param bundlePaths - Array of paths to compiled bundles (e.g., ['/_assets/core.js', '/_assets/docs.js'])
|
|
330
|
+
* @returns HTML with injected script tags
|
|
202
331
|
*
|
|
203
332
|
* @example
|
|
204
333
|
* ```typescript
|
|
205
334
|
* const html = '<html><body>Content</body></html>';
|
|
206
|
-
* const enhanced =
|
|
207
|
-
* // Returns
|
|
335
|
+
* const enhanced = autoInjectBundles(html, ['/_assets/core.js', '/_assets/docs.js']);
|
|
336
|
+
* // Returns HTML with both script tags injected before </body>
|
|
208
337
|
* ```
|
|
209
338
|
*/
|
|
210
|
-
export function
|
|
211
|
-
if (!
|
|
212
|
-
return html;
|
|
213
|
-
}
|
|
214
|
-
// Sanitize bundlePath to prevent XSS attacks
|
|
215
|
-
// Only allow safe characters: alphanumeric, hyphens, underscores, dots, and forward slashes
|
|
216
|
-
// Must start with / and end with .js
|
|
217
|
-
const safePathPattern = /^\/[\w\-./]+\.js$/;
|
|
218
|
-
if (!safePathPattern.test(bundlePath)) {
|
|
219
|
-
// Invalid path format, skip injection to prevent potential XSS
|
|
339
|
+
export function autoInjectBundles(html, bundlePaths) {
|
|
340
|
+
if (!bundlePaths || bundlePaths.length === 0) {
|
|
220
341
|
return html;
|
|
221
342
|
}
|
|
222
|
-
//
|
|
223
|
-
|
|
343
|
+
// Filter paths: must be valid and not already included in HTML
|
|
344
|
+
const validPaths = bundlePaths.filter((bundlePath) => isValidBundlePath(bundlePath) && !isBundleAlreadyIncluded(html, bundlePath));
|
|
345
|
+
if (validPaths.length === 0) {
|
|
224
346
|
return html;
|
|
225
347
|
}
|
|
226
348
|
// Find </body> tag (case-insensitive)
|
|
@@ -231,7 +353,9 @@ export function autoInjectBundle(html, bundlePath) {
|
|
|
231
353
|
const bodyClosePos = bodyCloseMatch.index;
|
|
232
354
|
const before = html.substring(0, bodyClosePos);
|
|
233
355
|
const after = html.substring(bodyClosePos);
|
|
234
|
-
// Inject script
|
|
235
|
-
const
|
|
236
|
-
|
|
356
|
+
// Inject all script tags before </body>
|
|
357
|
+
const scriptTags = validPaths
|
|
358
|
+
.map((bundlePath) => `<script type="module" src="${bundlePath}"></script>`)
|
|
359
|
+
.join('\n');
|
|
360
|
+
return `${before}${scriptTags}\n${after}`;
|
|
237
361
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* await build({ clean: true });
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
-
export type { StatiConfig, PageModel, FrontMatter, BuildContext, PageContext, BuildHooks, NavNode, ISGConfig, AgingRule, BuildStats, } from './types/index.js';
|
|
22
|
+
export type { StatiConfig, PageModel, FrontMatter, BuildContext, PageContext, BuildHooks, NavNode, ISGConfig, AgingRule, BuildStats, BundleConfig, } from './types/index.js';
|
|
23
23
|
export type { SEOMetadata, SEOConfig, SEOContext, SEOValidationResult, SEOTagType, RobotsConfig, OpenGraphConfig, OpenGraphImage, OpenGraphArticle, TwitterCardConfig, AuthorConfig, } from './types/index.js';
|
|
24
24
|
export type { SitemapConfig, SitemapEntry, SitemapGenerationResult, ChangeFrequency, } from './types/index.js';
|
|
25
25
|
export type { RSSConfig, RSSFeedConfig, RSSGenerationResult } from './types/index.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,YAAY,EACV,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,YAAY,EACV,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,UAAU,EACV,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EACV,aAAa,EACb,YAAY,EACZ,uBAAuB,EACvB,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAGtF,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG1F,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,uBAAuB,EACvB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,EACvB,iBAAiB,EACjB,2BAA2B,EAC3B,UAAU,EACV,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAE7D"}
|