create-dalila 1.2.14 → 1.2.15
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 +6 -2
- package/index.js +11 -0
- package/package.json +1 -1
- package/template/build.mjs +1040 -0
- package/template/index.html +7 -0
- package/template/package.json +3 -2
- package/template/src/main.ts +17 -0
package/README.md
CHANGED
|
@@ -23,11 +23,14 @@ Open http://localhost:4242 to see your app.
|
|
|
23
23
|
- Dev server + route generation watcher
|
|
24
24
|
- TypeScript support out of the box
|
|
25
25
|
- Minimal CSS styling
|
|
26
|
+
- `dompurify` installed and wired into the runtime bootstrap
|
|
27
|
+
- Trusted Types enabled in the starter runtime config
|
|
26
28
|
|
|
27
29
|
## Project Structure
|
|
28
30
|
|
|
29
31
|
```
|
|
30
32
|
my-app/
|
|
33
|
+
├── build.mjs # Packages a standalone dist/ preview build
|
|
31
34
|
├── dev.mjs # Runs route watcher + dev server
|
|
32
35
|
├── index.html # App shell
|
|
33
36
|
├── src/
|
|
@@ -35,7 +38,7 @@ my-app/
|
|
|
35
38
|
│ │ ├── layout.html
|
|
36
39
|
│ │ ├── page.html
|
|
37
40
|
│ │ └── page.ts
|
|
38
|
-
│ ├── main.ts # Router bootstrap
|
|
41
|
+
│ ├── main.ts # Router bootstrap + runtime security defaults
|
|
39
42
|
│ └── style.css # Styles
|
|
40
43
|
├── package.json
|
|
41
44
|
└── tsconfig.json
|
|
@@ -46,7 +49,8 @@ my-app/
|
|
|
46
49
|
- `npm run dev` - Start dev server and route watcher
|
|
47
50
|
- `npm run routes` - Generate route files once
|
|
48
51
|
- `npm run routes:watch` - Watch route files and regenerate outputs
|
|
49
|
-
- `npm run build` -
|
|
52
|
+
- `npm run build` - Generate routes, compile TypeScript, and package a standalone `dist/`
|
|
53
|
+
- `npm run preview` - Serve the built `dist/` output locally
|
|
50
54
|
|
|
51
55
|
## Learn More
|
|
52
56
|
|
package/index.js
CHANGED
|
@@ -131,6 +131,16 @@ function updatePackageJson(projectPath, projectName) {
|
|
|
131
131
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function updateTemplatePlaceholders(projectPath, projectName) {
|
|
135
|
+
const trustedTypesPolicyName = `${projectName}-html`;
|
|
136
|
+
const mainPath = path.join(projectPath, 'src', 'main.ts');
|
|
137
|
+
const source = fs.readFileSync(mainPath, 'utf8');
|
|
138
|
+
fs.writeFileSync(
|
|
139
|
+
mainPath,
|
|
140
|
+
source.replaceAll('__DALILA_TRUSTED_TYPES_POLICY__', trustedTypesPolicyName)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
function main() {
|
|
135
145
|
ensureSupportedNode();
|
|
136
146
|
|
|
@@ -184,6 +194,7 @@ function main() {
|
|
|
184
194
|
|
|
185
195
|
// Update package.json with project name
|
|
186
196
|
updatePackageJson(projectPath, projectName);
|
|
197
|
+
updateTemplatePlaceholders(projectPath, projectName);
|
|
187
198
|
|
|
188
199
|
// Success message
|
|
189
200
|
console.log(`${green('Done!')} Created ${bold(projectName)}\n`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const FOUC_PREVENTION_STYLE = ` <style>[d-loading]{visibility:hidden}</style>`;
|
|
9
|
+
const SCRIPT_SOURCE_EXTENSIONS = new Set(['.ts', '.mts', '.cts']);
|
|
10
|
+
const SCRIPT_REQUEST_SOURCE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
11
|
+
const STATIC_DIR_EXCLUDES = new Set([
|
|
12
|
+
'src',
|
|
13
|
+
'public',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'dist',
|
|
16
|
+
'coverage',
|
|
17
|
+
'playwright-report',
|
|
18
|
+
'test-results',
|
|
19
|
+
]);
|
|
20
|
+
const HTML_ENTRY_DIR_EXCLUDES = new Set([
|
|
21
|
+
'node_modules',
|
|
22
|
+
'dist',
|
|
23
|
+
'coverage',
|
|
24
|
+
'playwright-report',
|
|
25
|
+
'test-results',
|
|
26
|
+
]);
|
|
27
|
+
const STATIC_FILE_EXCLUDES = new Set([
|
|
28
|
+
'package.json',
|
|
29
|
+
'package-lock.json',
|
|
30
|
+
'bun.lock',
|
|
31
|
+
'bun.lockb',
|
|
32
|
+
'pnpm-lock.yaml',
|
|
33
|
+
'yarn.lock',
|
|
34
|
+
'tsconfig.json',
|
|
35
|
+
'build.mjs',
|
|
36
|
+
'dev.mjs',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function resolvePackageModule(moduleName, projectDir) {
|
|
40
|
+
try {
|
|
41
|
+
return require.resolve(moduleName, { paths: [projectDir] });
|
|
42
|
+
} catch {
|
|
43
|
+
return require.resolve(moduleName);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isScriptSourceFile(filePath) {
|
|
48
|
+
return SCRIPT_SOURCE_EXTENSIONS.has(path.extname(filePath)) && !filePath.endsWith('.d.ts');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function walkFiles(dir, files = []) {
|
|
52
|
+
if (!fs.existsSync(dir)) return files;
|
|
53
|
+
|
|
54
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const entryPath = path.join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
walkFiles(entryPath, files);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
files.push(entryPath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return files;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function detectPreloadScripts(baseDir) {
|
|
69
|
+
const preloads = [];
|
|
70
|
+
|
|
71
|
+
for (const filePath of walkFiles(baseDir)) {
|
|
72
|
+
if (!isScriptSourceFile(filePath)) continue;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
76
|
+
const persistRegex = /persist\s*\(\s*signal\s*(?:<[^>]+>)?\s*\(\s*['"]?([^'")]+)['"]?\s*\)\s*,\s*\{([^}]+)\}\s*\)/g;
|
|
77
|
+
|
|
78
|
+
let match;
|
|
79
|
+
while ((match = persistRegex.exec(source)) !== null) {
|
|
80
|
+
const defaultValue = match[1];
|
|
81
|
+
const options = match[2];
|
|
82
|
+
|
|
83
|
+
if (!options.includes('preload') || !options.match(/preload\s*:\s*true/)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nameMatch = options.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
88
|
+
if (!nameMatch) continue;
|
|
89
|
+
|
|
90
|
+
let storageType = 'localStorage';
|
|
91
|
+
const storageMatch = options.match(/storage\s*:\s*(\w+)/);
|
|
92
|
+
if (storageMatch && storageMatch[1] === 'sessionStorage') {
|
|
93
|
+
storageType = 'sessionStorage';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
preloads.push({
|
|
97
|
+
name: nameMatch[1],
|
|
98
|
+
defaultValue,
|
|
99
|
+
storageType,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Ignore malformed files during packaging.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return preloads;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function generatePreloadScript(name, defaultValue, storageType = 'localStorage') {
|
|
111
|
+
const k = JSON.stringify(name);
|
|
112
|
+
const d = JSON.stringify(defaultValue);
|
|
113
|
+
const script = `(function(){try{var v=${storageType}.getItem(${k});document.documentElement.setAttribute('data-theme',v?JSON.parse(v):${d})}catch(e){document.documentElement.setAttribute('data-theme',${d})}})();`;
|
|
114
|
+
|
|
115
|
+
return script
|
|
116
|
+
.replace(/</g, '\\x3C')
|
|
117
|
+
.replace(/-->/g, '--\\x3E')
|
|
118
|
+
.replace(/\u2028/g, '\\u2028')
|
|
119
|
+
.replace(/\u2029/g, '\\u2029');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderPreloadScriptTags(baseDir) {
|
|
123
|
+
const preloads = detectPreloadScripts(baseDir);
|
|
124
|
+
if (preloads.length === 0) return '';
|
|
125
|
+
|
|
126
|
+
return preloads
|
|
127
|
+
.map((preload) => ` <script>${generatePreloadScript(preload.name, preload.defaultValue, preload.storageType)}</script>`)
|
|
128
|
+
.join('\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ensureFileExists(filePath, label) {
|
|
132
|
+
if (!fs.existsSync(filePath)) {
|
|
133
|
+
throw new Error(`Missing ${label}: ${filePath}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function copyDirectoryContents(sourceDir, destinationDir) {
|
|
138
|
+
if (!fs.existsSync(sourceDir)) return;
|
|
139
|
+
fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
|
|
140
|
+
fs.cpSync(sourceDir, destinationDir, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function copyStaticSourceAssets(sourceDir, destinationDir) {
|
|
144
|
+
if (!fs.existsSync(sourceDir)) return;
|
|
145
|
+
|
|
146
|
+
for (const filePath of walkFiles(sourceDir)) {
|
|
147
|
+
if (isScriptSourceFile(filePath) || filePath.endsWith('.d.ts')) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const relativePath = path.relative(sourceDir, filePath);
|
|
152
|
+
const targetPath = path.join(destinationDir, relativePath);
|
|
153
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
154
|
+
fs.copyFileSync(filePath, targetPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveDalilaPackageRoot(projectDir) {
|
|
159
|
+
const dalilaEntry = require.resolve('dalila', { paths: [projectDir] });
|
|
160
|
+
return path.dirname(path.dirname(dalilaEntry));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function formatTypeScriptError(ts, diagnostic) {
|
|
164
|
+
return ts.formatDiagnostic(diagnostic, {
|
|
165
|
+
getCanonicalFileName: (value) => value,
|
|
166
|
+
getCurrentDirectory: () => process.cwd(),
|
|
167
|
+
getNewLine: () => '\n',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseTypeScriptConfig(ts, configPath) {
|
|
172
|
+
let unrecoverableDiagnostic = null;
|
|
173
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(
|
|
174
|
+
configPath,
|
|
175
|
+
{},
|
|
176
|
+
{
|
|
177
|
+
...ts.sys,
|
|
178
|
+
onUnRecoverableConfigFileDiagnostic(diagnostic) {
|
|
179
|
+
unrecoverableDiagnostic = diagnostic;
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (unrecoverableDiagnostic) {
|
|
185
|
+
throw new Error(formatTypeScriptError(ts, unrecoverableDiagnostic));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!parsed) {
|
|
189
|
+
throw new Error(`Failed to parse TypeScript config: ${configPath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (parsed.errors?.length) {
|
|
193
|
+
throw new Error(parsed.errors.map((diagnostic) => formatTypeScriptError(ts, diagnostic)).join('\n'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return parsed;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function inferCommonSourceDir(fileNames, fallbackDir) {
|
|
200
|
+
const sourceDirs = fileNames
|
|
201
|
+
.filter((filePath) => !filePath.endsWith('.d.ts'))
|
|
202
|
+
.map((filePath) => path.dirname(path.resolve(filePath)));
|
|
203
|
+
|
|
204
|
+
if (sourceDirs.length === 0) {
|
|
205
|
+
return fallbackDir;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let commonDir = sourceDirs[0];
|
|
209
|
+
for (const nextDir of sourceDirs.slice(1)) {
|
|
210
|
+
while (commonDir !== path.dirname(commonDir)) {
|
|
211
|
+
const relativePath = path.relative(commonDir, nextDir);
|
|
212
|
+
if (relativePath === '' || isRelativePathInsideBase(relativePath)) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
commonDir = path.dirname(commonDir);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const relativePath = path.relative(commonDir, nextDir);
|
|
219
|
+
if (relativePath !== '' && !isRelativePathInsideBase(relativePath)) {
|
|
220
|
+
return fallbackDir;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return commonDir;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function loadTypeScriptBuildConfig(projectDir) {
|
|
228
|
+
const ts = require(resolvePackageModule('typescript', projectDir));
|
|
229
|
+
const configPath = ts.findConfigFile(projectDir, ts.sys.fileExists, 'tsconfig.json');
|
|
230
|
+
const packageOutDirAbs = path.join(projectDir, 'dist');
|
|
231
|
+
const defaultSourceDirAbs = fs.existsSync(path.join(projectDir, 'src'))
|
|
232
|
+
? path.join(projectDir, 'src')
|
|
233
|
+
: projectDir;
|
|
234
|
+
|
|
235
|
+
if (!configPath) {
|
|
236
|
+
return {
|
|
237
|
+
projectDir,
|
|
238
|
+
configPath: null,
|
|
239
|
+
configDir: projectDir,
|
|
240
|
+
compileOutDirAbs: packageOutDirAbs,
|
|
241
|
+
packageOutDirAbs,
|
|
242
|
+
rootDirAbs: projectDir,
|
|
243
|
+
sourceDirAbs: defaultSourceDirAbs,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const configDir = path.dirname(configPath);
|
|
248
|
+
const parsed = parseTypeScriptConfig(ts, configPath);
|
|
249
|
+
const inferredRootDirAbs = inferCommonSourceDir(parsed.fileNames, projectDir);
|
|
250
|
+
const explicitRootDirAbs = typeof parsed.options.rootDir === 'string'
|
|
251
|
+
? path.resolve(configDir, parsed.options.rootDir)
|
|
252
|
+
: null;
|
|
253
|
+
const sourceDirAbs = explicitRootDirAbs
|
|
254
|
+
?? (defaultSourceDirAbs !== projectDir ? defaultSourceDirAbs : inferredRootDirAbs);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
projectDir,
|
|
258
|
+
configPath,
|
|
259
|
+
configDir,
|
|
260
|
+
compileOutDirAbs: parsed.options.outDir
|
|
261
|
+
? path.resolve(configDir, parsed.options.outDir)
|
|
262
|
+
: packageOutDirAbs,
|
|
263
|
+
packageOutDirAbs,
|
|
264
|
+
rootDirAbs: explicitRootDirAbs ?? inferredRootDirAbs,
|
|
265
|
+
sourceDirAbs,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveExportTarget(target) {
|
|
270
|
+
if (typeof target === 'string') return target;
|
|
271
|
+
if (!target || typeof target !== 'object') return null;
|
|
272
|
+
return target.default || target.import || null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildDalilaImportEntries(projectDir) {
|
|
276
|
+
const dalilaRoot = resolveDalilaPackageRoot(projectDir);
|
|
277
|
+
const dalilaPackageJson = JSON.parse(
|
|
278
|
+
fs.readFileSync(path.join(dalilaRoot, 'package.json'), 'utf8')
|
|
279
|
+
);
|
|
280
|
+
const distRoot = path.join(dalilaRoot, 'dist');
|
|
281
|
+
const distIndexPath = path.join(distRoot, 'index.js');
|
|
282
|
+
const imports = {};
|
|
283
|
+
|
|
284
|
+
for (const [subpath, target] of Object.entries(dalilaPackageJson.exports ?? {})) {
|
|
285
|
+
const exportTarget = resolveExportTarget(target);
|
|
286
|
+
if (!exportTarget || !exportTarget.endsWith('.js')) continue;
|
|
287
|
+
|
|
288
|
+
const absoluteTarget = path.resolve(dalilaRoot, exportTarget);
|
|
289
|
+
if (absoluteTarget !== distIndexPath && !absoluteTarget.startsWith(distRoot + path.sep)) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const relativeTarget = path.relative(distRoot, absoluteTarget).replace(/\\/g, '/');
|
|
294
|
+
const specifier = subpath === '.' ? 'dalila' : `dalila/${subpath.slice(2)}`;
|
|
295
|
+
imports[specifier] = `/vendor/dalila/${relativeTarget}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return imports;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeNodeModulesImportTarget(target, projectDir, baseDirAbs = projectDir) {
|
|
302
|
+
if (typeof target !== 'string') return null;
|
|
303
|
+
|
|
304
|
+
const { pathname, suffix } = splitUrlTarget(target);
|
|
305
|
+
if (isUrlWithScheme(pathname)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (pathname.startsWith('/node_modules/')) {
|
|
310
|
+
return {
|
|
311
|
+
relativeTarget: pathname.slice('/node_modules/'.length),
|
|
312
|
+
suffix,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (pathname.startsWith('node_modules/')) {
|
|
317
|
+
return {
|
|
318
|
+
relativeTarget: pathname.slice('node_modules/'.length),
|
|
319
|
+
suffix,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!pathname.startsWith('./') && !pathname.startsWith('../')) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const nodeModulesRoot = path.join(projectDir, 'node_modules');
|
|
328
|
+
const resolvedAbsPath = resolveLocalProjectPath(projectDir, pathname, baseDirAbs);
|
|
329
|
+
const relativeTarget = path.relative(nodeModulesRoot, resolvedAbsPath);
|
|
330
|
+
if (!isRelativePathInsideBase(relativeTarget)) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
relativeTarget: toPosixPath(relativeTarget),
|
|
336
|
+
suffix,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function splitUrlTarget(target) {
|
|
341
|
+
const match = String(target).match(/^([^?#]*)([?#].*)?$/);
|
|
342
|
+
return {
|
|
343
|
+
pathname: match?.[1] ?? String(target),
|
|
344
|
+
suffix: match?.[2] ?? '',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isUrlWithScheme(pathname) {
|
|
349
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(pathname) || pathname.startsWith('//');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function isLocalUrlPath(pathname) {
|
|
353
|
+
return pathname.startsWith('/') || pathname.startsWith('./') || pathname.startsWith('../');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function resolveLocalProjectPath(projectDir, pathname, baseDirAbs = projectDir) {
|
|
357
|
+
if (pathname.startsWith('/')) {
|
|
358
|
+
return path.resolve(projectDir, pathname.slice(1));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return path.resolve(baseDirAbs, pathname);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function emittedExtensionCandidates(sourceExt) {
|
|
365
|
+
switch (sourceExt) {
|
|
366
|
+
case '.mts':
|
|
367
|
+
return ['.mjs', '.js'];
|
|
368
|
+
case '.cts':
|
|
369
|
+
return ['.cjs', '.js'];
|
|
370
|
+
case '.ts':
|
|
371
|
+
return ['.js'];
|
|
372
|
+
default:
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function replaceExtension(filePath, nextExt) {
|
|
378
|
+
return `${filePath.slice(0, -path.extname(filePath).length)}${nextExt}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function toPosixPath(value) {
|
|
382
|
+
return value.replace(/\\/g, '/');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function isRelativePathInsideBase(relativePath) {
|
|
386
|
+
return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function getProjectRelativeCandidates(targetAbsPath, buildConfig) {
|
|
390
|
+
const relativeCandidates = [];
|
|
391
|
+
const relativeToRootDir = path.relative(buildConfig.rootDirAbs, targetAbsPath);
|
|
392
|
+
if (isRelativePathInsideBase(relativeToRootDir)) {
|
|
393
|
+
relativeCandidates.push(relativeToRootDir);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const relativeToProject = path.relative(buildConfig.projectDir, targetAbsPath);
|
|
397
|
+
if (isRelativePathInsideBase(relativeToProject)) {
|
|
398
|
+
relativeCandidates.push(relativeToProject);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return [...new Set(relativeCandidates)];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function resolveCompiledSourceOutputPath(sourceAbsPath, buildConfig) {
|
|
405
|
+
const sourceExt = path.extname(sourceAbsPath);
|
|
406
|
+
const candidateExts = emittedExtensionCandidates(sourceExt);
|
|
407
|
+
if (candidateExts.length === 0) return null;
|
|
408
|
+
|
|
409
|
+
for (const relativeCandidate of getProjectRelativeCandidates(sourceAbsPath, buildConfig)) {
|
|
410
|
+
for (const candidateExt of candidateExts) {
|
|
411
|
+
const compiledAbsPath = path.join(
|
|
412
|
+
buildConfig.compileOutDirAbs,
|
|
413
|
+
replaceExtension(relativeCandidate, candidateExt)
|
|
414
|
+
);
|
|
415
|
+
if (fs.existsSync(compiledAbsPath)) {
|
|
416
|
+
return compiledAbsPath;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function toPackagedUrlFromCompiledOutput(compiledAbsPath, buildConfig) {
|
|
425
|
+
return `/${toPosixPath(path.relative(buildConfig.compileOutDirAbs, compiledAbsPath))}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function resolvePackagedProjectUrlPath(targetAbsPath, buildConfig) {
|
|
429
|
+
const [relativeCandidate] = getProjectRelativeCandidates(targetAbsPath, buildConfig);
|
|
430
|
+
if (relativeCandidate == null) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!relativeCandidate) {
|
|
435
|
+
return '/';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return `/${toPosixPath(relativeCandidate)}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function ensureTrailingSlash(urlPath) {
|
|
442
|
+
return urlPath.endsWith('/') ? urlPath : `${urlPath}/`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildUserProjectImportEntries(buildConfig) {
|
|
446
|
+
const sourceDirUrl = resolvePackagedProjectUrlPath(buildConfig.sourceDirAbs, buildConfig) ?? '/src/';
|
|
447
|
+
return {
|
|
448
|
+
'@/': ensureTrailingSlash(sourceDirUrl),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getScriptSourceRequestCandidates(requestAbsPath) {
|
|
453
|
+
const requestExt = path.extname(requestAbsPath);
|
|
454
|
+
if (SCRIPT_SOURCE_EXTENSIONS.has(requestExt)) {
|
|
455
|
+
return {
|
|
456
|
+
candidates: [requestAbsPath],
|
|
457
|
+
requireExistingFile: false,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!SCRIPT_REQUEST_SOURCE_EXTENSIONS.has(requestExt)) {
|
|
462
|
+
return {
|
|
463
|
+
candidates: [],
|
|
464
|
+
requireExistingFile: true,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
switch (requestExt) {
|
|
469
|
+
case '.mjs':
|
|
470
|
+
return {
|
|
471
|
+
candidates: [replaceExtension(requestAbsPath, '.mts'), replaceExtension(requestAbsPath, '.ts')],
|
|
472
|
+
requireExistingFile: true,
|
|
473
|
+
};
|
|
474
|
+
case '.cjs':
|
|
475
|
+
return {
|
|
476
|
+
candidates: [replaceExtension(requestAbsPath, '.cts'), replaceExtension(requestAbsPath, '.ts')],
|
|
477
|
+
requireExistingFile: true,
|
|
478
|
+
};
|
|
479
|
+
case '.js':
|
|
480
|
+
default:
|
|
481
|
+
return {
|
|
482
|
+
candidates: [
|
|
483
|
+
replaceExtension(requestAbsPath, '.ts'),
|
|
484
|
+
replaceExtension(requestAbsPath, '.mts'),
|
|
485
|
+
replaceExtension(requestAbsPath, '.cts'),
|
|
486
|
+
],
|
|
487
|
+
requireExistingFile: true,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resolveLocalSourceScriptPath(target, buildConfig, baseDirAbs = buildConfig.projectDir) {
|
|
493
|
+
const { pathname } = splitUrlTarget(target);
|
|
494
|
+
if (!isLocalUrlPath(pathname) || isUrlWithScheme(pathname)) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const requestAbsPath = resolveLocalProjectPath(buildConfig.projectDir, pathname, baseDirAbs);
|
|
499
|
+
const { candidates, requireExistingFile } = getScriptSourceRequestCandidates(requestAbsPath);
|
|
500
|
+
for (const candidateAbsPath of candidates) {
|
|
501
|
+
if (!requireExistingFile || fs.existsSync(candidateAbsPath)) {
|
|
502
|
+
return candidateAbsPath;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function rewriteLocalSourceTarget(target, buildConfig, baseDirAbs = buildConfig.projectDir) {
|
|
510
|
+
const { suffix } = splitUrlTarget(target);
|
|
511
|
+
const sourceAbsPath = resolveLocalSourceScriptPath(target, buildConfig, baseDirAbs);
|
|
512
|
+
if (!sourceAbsPath) {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const compiledAbsPath = resolveCompiledSourceOutputPath(sourceAbsPath, buildConfig);
|
|
517
|
+
if (!compiledAbsPath) {
|
|
518
|
+
throw new Error(`Compiled output not found for source target "${target}"`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return `${toPackagedUrlFromCompiledOutput(compiledAbsPath, buildConfig)}${suffix}`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function rewriteImportMapScopeName(scopeName, buildConfig, baseDirAbs = buildConfig.projectDir) {
|
|
525
|
+
const rewrittenSourceTarget = rewriteLocalSourceTarget(scopeName, buildConfig, baseDirAbs);
|
|
526
|
+
if (rewrittenSourceTarget) {
|
|
527
|
+
return rewrittenSourceTarget;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const { pathname, suffix } = splitUrlTarget(scopeName);
|
|
531
|
+
if (!isLocalUrlPath(pathname) || isUrlWithScheme(pathname)) {
|
|
532
|
+
return scopeName;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const packagedUrlPath = resolvePackagedProjectUrlPath(
|
|
536
|
+
resolveLocalProjectPath(buildConfig.projectDir, pathname, baseDirAbs),
|
|
537
|
+
buildConfig
|
|
538
|
+
);
|
|
539
|
+
if (!packagedUrlPath) {
|
|
540
|
+
return scopeName;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const needsTrailingSlash = pathname.endsWith('/') && packagedUrlPath !== '/' && !packagedUrlPath.endsWith('/');
|
|
544
|
+
return `${packagedUrlPath}${needsTrailingSlash ? '/' : ''}${suffix}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function getPackagePathParts(relativeTarget) {
|
|
548
|
+
const segments = relativeTarget.split('/').filter(Boolean);
|
|
549
|
+
if (segments.length === 0) {
|
|
550
|
+
throw new Error(`Invalid node_modules import target: "${relativeTarget}"`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (segments[0].startsWith('@')) {
|
|
554
|
+
if (segments.length < 2) {
|
|
555
|
+
throw new Error(`Invalid scoped package import target: "${relativeTarget}"`);
|
|
556
|
+
}
|
|
557
|
+
return segments.slice(0, 2);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return segments.slice(0, 1);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function rewriteImportMapTarget(projectDir, vendorDir, target, buildConfig, copiedPackages, baseDirAbs = projectDir) {
|
|
564
|
+
const normalizedTarget = normalizeNodeModulesImportTarget(target, projectDir, baseDirAbs);
|
|
565
|
+
if (normalizedTarget) {
|
|
566
|
+
const relativeTarget = normalizedTarget.relativeTarget;
|
|
567
|
+
const packagePathParts = getPackagePathParts(relativeTarget);
|
|
568
|
+
const packagePath = path.join(...packagePathParts);
|
|
569
|
+
const sourcePackageDir = path.join(projectDir, 'node_modules', packagePath);
|
|
570
|
+
const destinationPackageDir = path.join(vendorDir, 'node_modules', packagePath);
|
|
571
|
+
ensureFileExists(sourcePackageDir, `import-map package for target "${target}"`);
|
|
572
|
+
|
|
573
|
+
if (!copiedPackages.has(packagePath)) {
|
|
574
|
+
copyDirectoryContents(sourcePackageDir, destinationPackageDir);
|
|
575
|
+
copiedPackages.add(packagePath);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return `/vendor/node_modules/${relativeTarget}${normalizedTarget.suffix}`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const rewrittenSourceTarget = rewriteLocalSourceTarget(target, buildConfig, baseDirAbs);
|
|
582
|
+
if (rewrittenSourceTarget) {
|
|
583
|
+
return rewrittenSourceTarget;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return target;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function packageExistingImportMapImports(projectDir, vendorDir, imports, buildConfig, copiedPackages = new Set(), baseDirAbs = projectDir) {
|
|
590
|
+
const rewrittenImports = {};
|
|
591
|
+
|
|
592
|
+
for (const [specifier, target] of Object.entries(imports ?? {})) {
|
|
593
|
+
rewrittenImports[specifier] = rewriteImportMapTarget(projectDir, vendorDir, target, buildConfig, copiedPackages, baseDirAbs);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return rewrittenImports;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function packageExistingImportMapScopes(projectDir, vendorDir, scopes, buildConfig, copiedPackages = new Set(), baseDirAbs = projectDir) {
|
|
600
|
+
const rewrittenScopes = {};
|
|
601
|
+
|
|
602
|
+
for (const [scopeName, scopeImports] of Object.entries(scopes ?? {})) {
|
|
603
|
+
const rewrittenScopeName = rewriteImportMapScopeName(scopeName, buildConfig, baseDirAbs);
|
|
604
|
+
if (!scopeImports || typeof scopeImports !== 'object' || Array.isArray(scopeImports)) {
|
|
605
|
+
rewrittenScopes[rewrittenScopeName] = scopeImports;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const rewrittenScopeImports = packageExistingImportMapImports(
|
|
610
|
+
projectDir,
|
|
611
|
+
vendorDir,
|
|
612
|
+
scopeImports,
|
|
613
|
+
buildConfig,
|
|
614
|
+
copiedPackages,
|
|
615
|
+
baseDirAbs
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
rewrittenScopes[rewrittenScopeName] = {
|
|
619
|
+
...(rewrittenScopes[rewrittenScopeName] ?? {}),
|
|
620
|
+
...rewrittenScopeImports,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return rewrittenScopes;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function extractImportMap(html) {
|
|
628
|
+
const importMapPattern = /<script[^>]*type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/i;
|
|
629
|
+
const match = html.match(importMapPattern);
|
|
630
|
+
if (!match) {
|
|
631
|
+
return {
|
|
632
|
+
html,
|
|
633
|
+
importMap: { imports: {} },
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let importMap = { imports: {} };
|
|
638
|
+
try {
|
|
639
|
+
const parsed = JSON.parse(match[1]);
|
|
640
|
+
if (parsed && typeof parsed === 'object') {
|
|
641
|
+
importMap = parsed;
|
|
642
|
+
}
|
|
643
|
+
} catch {
|
|
644
|
+
// Ignore invalid import maps and replace them with the packaged version.
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
html: html.replace(match[0], '').trimEnd() + '\n',
|
|
649
|
+
importMap,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function renderImportMapScript(importMap) {
|
|
654
|
+
const payload = JSON.stringify(importMap, null, 2)
|
|
655
|
+
.split('\n')
|
|
656
|
+
.map(line => ` ${line}`)
|
|
657
|
+
.join('\n');
|
|
658
|
+
|
|
659
|
+
return ` <script type="importmap">\n${payload}\n </script>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function shouldPackageHtmlEntry(source) {
|
|
663
|
+
return /<script[^>]*type=["']module["'][^>]*>/i.test(source)
|
|
664
|
+
|| /<script[^>]*type=["']importmap["'][^>]*>/i.test(source);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function injectHeadContent(html, fragments) {
|
|
668
|
+
const content = fragments.filter(Boolean).join('\n');
|
|
669
|
+
if (!content) return html;
|
|
670
|
+
|
|
671
|
+
const headOpenMatch = html.match(/<head\b[^>]*>/i);
|
|
672
|
+
const headCloseMatch = html.match(/<\/head>/i);
|
|
673
|
+
if (!headOpenMatch || headOpenMatch.index == null || !headCloseMatch || headCloseMatch.index == null) {
|
|
674
|
+
return `${content}\n${html}`;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const headStart = headOpenMatch.index + headOpenMatch[0].length;
|
|
678
|
+
const headEnd = headCloseMatch.index;
|
|
679
|
+
const headContent = html.slice(headStart, headEnd);
|
|
680
|
+
const moduleScriptMatch = headContent.match(/<script\b[^>]*\btype=["']module["'][^>]*>/i);
|
|
681
|
+
const stylesheetMatch = headContent.match(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/i);
|
|
682
|
+
const insertionOffset = moduleScriptMatch?.index
|
|
683
|
+
?? stylesheetMatch?.index
|
|
684
|
+
?? headContent.length;
|
|
685
|
+
const insertionIndex = headStart + insertionOffset;
|
|
686
|
+
|
|
687
|
+
return `${html.slice(0, insertionIndex)}\n${content}\n${html.slice(insertionIndex)}`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function rewriteInlineModuleSpecifiers(source, buildConfig, baseDirAbs = buildConfig.projectDir) {
|
|
691
|
+
const rewriteStaticSpecifierMatch = (fullMatch, prefix, quote, specifier) => {
|
|
692
|
+
const rewrittenSpecifier = rewriteLocalSourceTarget(specifier, buildConfig, baseDirAbs);
|
|
693
|
+
if (!rewrittenSpecifier) {
|
|
694
|
+
return fullMatch;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return `${prefix}${quote}${rewrittenSpecifier}${quote}`;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const rewriteDynamicSpecifierMatch = (fullMatch, prefix, quote, specifier, suffix) => {
|
|
701
|
+
const rewrittenSpecifier = rewriteLocalSourceTarget(specifier, buildConfig, baseDirAbs);
|
|
702
|
+
if (!rewrittenSpecifier) {
|
|
703
|
+
return fullMatch;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return `${prefix}${quote}${rewrittenSpecifier}${quote}${suffix}`;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
let rewrittenSource = source.replace(
|
|
710
|
+
/(\bimport\s*\(\s*)(['"])([^'"]+)\2(\s*\))/g,
|
|
711
|
+
rewriteDynamicSpecifierMatch
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
rewrittenSource = rewrittenSource.replace(
|
|
715
|
+
/(\bfrom\s*)(['"])([^'"]+)\2/g,
|
|
716
|
+
rewriteStaticSpecifierMatch
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
rewrittenSource = rewrittenSource.replace(
|
|
720
|
+
/(\bimport\s+)(['"])([^'"]+)\2/g,
|
|
721
|
+
rewriteStaticSpecifierMatch
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
return rewrittenSource;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function rewritePackagedModuleSpecifiers(buildConfig) {
|
|
728
|
+
for (const filePath of walkFiles(buildConfig.packageOutDirAbs)) {
|
|
729
|
+
if (!SCRIPT_REQUEST_SOURCE_EXTENSIONS.has(path.extname(filePath))) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
734
|
+
const rewrittenSource = rewriteInlineModuleSpecifiers(source, buildConfig);
|
|
735
|
+
if (rewrittenSource !== source) {
|
|
736
|
+
fs.writeFileSync(filePath, rewrittenSource);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function rewriteHtmlModuleScripts(html, buildConfig, baseDirAbs = buildConfig.projectDir) {
|
|
742
|
+
return html.replace(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi, (fullMatch, attrs, content) => {
|
|
743
|
+
const typeMatch = attrs.match(/\btype=["']([^"']+)["']/i);
|
|
744
|
+
if (!typeMatch || typeMatch[1] !== 'module') {
|
|
745
|
+
return fullMatch;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const srcMatch = attrs.match(/\bsrc=["']([^"']+)["']/i);
|
|
749
|
+
if (srcMatch) {
|
|
750
|
+
const rewrittenSrc = rewriteLocalSourceTarget(srcMatch[1], buildConfig, baseDirAbs);
|
|
751
|
+
if (!rewrittenSrc) {
|
|
752
|
+
return fullMatch;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return fullMatch.replace(srcMatch[0], `src="${rewrittenSrc}"`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const rewrittenContent = rewriteInlineModuleSpecifiers(content, buildConfig, baseDirAbs);
|
|
759
|
+
if (rewrittenContent === content) {
|
|
760
|
+
return fullMatch;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return `<script${attrs}>${rewrittenContent}</script>`;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function buildHtmlDocument(sourceHtmlPath, importEntries, buildConfig) {
|
|
768
|
+
const source = fs.readFileSync(sourceHtmlPath, 'utf8');
|
|
769
|
+
const { html: htmlWithoutImportMap } = extractImportMap(source);
|
|
770
|
+
const html = rewriteHtmlModuleScripts(htmlWithoutImportMap, buildConfig, path.dirname(sourceHtmlPath));
|
|
771
|
+
|
|
772
|
+
return injectHeadContent(html, [
|
|
773
|
+
FOUC_PREVENTION_STYLE,
|
|
774
|
+
renderPreloadScriptTags(buildConfig.sourceDirAbs),
|
|
775
|
+
renderImportMapScript(importEntries),
|
|
776
|
+
]);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function pathsOverlap(leftPath, rightPath) {
|
|
780
|
+
const normalizedLeft = path.resolve(leftPath);
|
|
781
|
+
const normalizedRight = path.resolve(rightPath);
|
|
782
|
+
|
|
783
|
+
return normalizedLeft === normalizedRight
|
|
784
|
+
|| normalizedLeft.startsWith(`${normalizedRight}${path.sep}`)
|
|
785
|
+
|| normalizedRight.startsWith(`${normalizedLeft}${path.sep}`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function copyCompiledOutputTree(buildConfig) {
|
|
789
|
+
if (buildConfig.compileOutDirAbs === buildConfig.packageOutDirAbs) {
|
|
790
|
+
ensureFileExists(buildConfig.packageOutDirAbs, 'compiled output directory');
|
|
791
|
+
return {
|
|
792
|
+
buildConfig,
|
|
793
|
+
cleanup() {},
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
ensureFileExists(buildConfig.compileOutDirAbs, 'compiled output directory');
|
|
798
|
+
let snapshotRoot = null;
|
|
799
|
+
let compileOutDirAbs = buildConfig.compileOutDirAbs;
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
if (pathsOverlap(buildConfig.compileOutDirAbs, buildConfig.packageOutDirAbs)) {
|
|
803
|
+
snapshotRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'dalila-build-'));
|
|
804
|
+
compileOutDirAbs = path.join(snapshotRoot, 'compiled');
|
|
805
|
+
copyDirectoryContents(buildConfig.compileOutDirAbs, compileOutDirAbs);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
fs.rmSync(buildConfig.packageOutDirAbs, { recursive: true, force: true });
|
|
809
|
+
copyDirectoryContents(compileOutDirAbs, buildConfig.packageOutDirAbs);
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
buildConfig: compileOutDirAbs === buildConfig.compileOutDirAbs
|
|
813
|
+
? buildConfig
|
|
814
|
+
: { ...buildConfig, compileOutDirAbs },
|
|
815
|
+
cleanup() {
|
|
816
|
+
if (snapshotRoot) {
|
|
817
|
+
fs.rmSync(snapshotRoot, { recursive: true, force: true });
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (snapshotRoot) {
|
|
823
|
+
fs.rmSync(snapshotRoot, { recursive: true, force: true });
|
|
824
|
+
}
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function getTopLevelProjectDirName(projectDir, targetDirAbs) {
|
|
830
|
+
const relativePath = path.relative(projectDir, targetDirAbs);
|
|
831
|
+
if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const [topLevelName] = relativePath.split(path.sep);
|
|
836
|
+
return topLevelName || null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function collectTopLevelStaticDirs(projectDir, buildConfig) {
|
|
840
|
+
const excludedNames = new Set(STATIC_DIR_EXCLUDES);
|
|
841
|
+
const sourceTopLevelDir = getTopLevelProjectDirName(projectDir, buildConfig.rootDirAbs);
|
|
842
|
+
const compileTopLevelDir = getTopLevelProjectDirName(projectDir, buildConfig.compileOutDirAbs);
|
|
843
|
+
const packageTopLevelDir = getTopLevelProjectDirName(projectDir, buildConfig.packageOutDirAbs);
|
|
844
|
+
|
|
845
|
+
if (sourceTopLevelDir && buildConfig.rootDirAbs !== buildConfig.projectDir) {
|
|
846
|
+
excludedNames.add(sourceTopLevelDir);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (compileTopLevelDir) {
|
|
850
|
+
excludedNames.add(compileTopLevelDir);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (packageTopLevelDir) {
|
|
854
|
+
excludedNames.add(packageTopLevelDir);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return fs.readdirSync(projectDir, { withFileTypes: true })
|
|
858
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && !excludedNames.has(entry.name))
|
|
859
|
+
.map((entry) => entry.name);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function copyTopLevelStaticDirs(projectDir, packageOutDirAbs, buildConfig) {
|
|
863
|
+
for (const dirName of collectTopLevelStaticDirs(projectDir, buildConfig)) {
|
|
864
|
+
copyDirectoryContents(path.join(projectDir, dirName), path.join(packageOutDirAbs, dirName));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function copyTopLevelStaticFiles(projectDir, packageOutDirAbs) {
|
|
869
|
+
for (const entry of fs.readdirSync(projectDir, { withFileTypes: true })) {
|
|
870
|
+
if (!entry.isFile() || entry.name.startsWith('.') || STATIC_FILE_EXCLUDES.has(entry.name)) {
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const sourcePath = path.join(projectDir, entry.name);
|
|
875
|
+
const destinationPath = path.join(packageOutDirAbs, entry.name);
|
|
876
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
877
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function resolveSourceAssetRoots(projectDir, buildConfig) {
|
|
882
|
+
const sourceRoots = [];
|
|
883
|
+
const srcDir = path.join(projectDir, 'src');
|
|
884
|
+
|
|
885
|
+
if (buildConfig.rootDirAbs !== projectDir && fs.existsSync(buildConfig.rootDirAbs)) {
|
|
886
|
+
sourceRoots.push(buildConfig.rootDirAbs);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (fs.existsSync(srcDir)) {
|
|
890
|
+
sourceRoots.push(srcDir);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return [...new Set(sourceRoots)];
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function copyPackagedSourceAssets(projectDir, buildConfig) {
|
|
897
|
+
for (const sourceDir of resolveSourceAssetRoots(projectDir, buildConfig)) {
|
|
898
|
+
for (const filePath of walkFiles(sourceDir)) {
|
|
899
|
+
if (isScriptSourceFile(filePath) || filePath.endsWith('.d.ts')) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const destinationPaths = new Set();
|
|
904
|
+
const projectRelativePath = path.relative(projectDir, filePath);
|
|
905
|
+
if (isRelativePathInsideBase(projectRelativePath)) {
|
|
906
|
+
destinationPaths.add(path.join(buildConfig.packageOutDirAbs, projectRelativePath));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const packagedUrlPath = resolvePackagedProjectUrlPath(filePath, buildConfig);
|
|
910
|
+
if (packagedUrlPath && packagedUrlPath !== '/') {
|
|
911
|
+
destinationPaths.add(path.join(buildConfig.packageOutDirAbs, packagedUrlPath.slice(1)));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
for (const destinationPath of destinationPaths) {
|
|
915
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
916
|
+
fs.copyFileSync(filePath, destinationPath);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function walkProjectHtmlFiles(dir, files = []) {
|
|
923
|
+
if (!fs.existsSync(dir)) return files;
|
|
924
|
+
|
|
925
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
926
|
+
for (const entry of entries) {
|
|
927
|
+
if (entry.name.startsWith('.')) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const entryPath = path.join(dir, entry.name);
|
|
932
|
+
if (entry.isDirectory()) {
|
|
933
|
+
if (HTML_ENTRY_DIR_EXCLUDES.has(entry.name)) {
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
walkProjectHtmlFiles(entryPath, files);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
941
|
+
files.push(entryPath);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return files;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function packageHtmlEntryPoints(projectDir, vendorDir, buildConfig, copiedPackages) {
|
|
949
|
+
for (const sourceHtmlPath of walkProjectHtmlFiles(projectDir)) {
|
|
950
|
+
const source = fs.readFileSync(sourceHtmlPath, 'utf8');
|
|
951
|
+
if (!shouldPackageHtmlEntry(source)) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const { importMap: existingImportMap } = extractImportMap(source);
|
|
956
|
+
const existingImports = existingImportMap && typeof existingImportMap.imports === 'object'
|
|
957
|
+
? existingImportMap.imports
|
|
958
|
+
: {};
|
|
959
|
+
const existingScopes = existingImportMap && typeof existingImportMap.scopes === 'object'
|
|
960
|
+
? existingImportMap.scopes
|
|
961
|
+
: {};
|
|
962
|
+
const baseDirAbs = path.dirname(sourceHtmlPath);
|
|
963
|
+
const rewrittenImports = packageExistingImportMapImports(
|
|
964
|
+
projectDir,
|
|
965
|
+
vendorDir,
|
|
966
|
+
existingImports,
|
|
967
|
+
buildConfig,
|
|
968
|
+
copiedPackages,
|
|
969
|
+
baseDirAbs
|
|
970
|
+
);
|
|
971
|
+
const rewrittenScopes = packageExistingImportMapScopes(
|
|
972
|
+
projectDir,
|
|
973
|
+
vendorDir,
|
|
974
|
+
existingScopes,
|
|
975
|
+
buildConfig,
|
|
976
|
+
copiedPackages,
|
|
977
|
+
baseDirAbs
|
|
978
|
+
);
|
|
979
|
+
const importMap = {
|
|
980
|
+
...existingImportMap,
|
|
981
|
+
imports: {
|
|
982
|
+
...rewrittenImports,
|
|
983
|
+
...buildUserProjectImportEntries(buildConfig),
|
|
984
|
+
...buildDalilaImportEntries(projectDir),
|
|
985
|
+
},
|
|
986
|
+
scopes: rewrittenScopes,
|
|
987
|
+
};
|
|
988
|
+
const packagedHtml = buildHtmlDocument(sourceHtmlPath, importMap, buildConfig);
|
|
989
|
+
const packagedHtmlPath = path.join(buildConfig.packageOutDirAbs, path.relative(projectDir, sourceHtmlPath));
|
|
990
|
+
fs.mkdirSync(path.dirname(packagedHtmlPath), { recursive: true });
|
|
991
|
+
fs.writeFileSync(packagedHtmlPath, packagedHtml);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export async function buildProject(projectDir = process.cwd()) {
|
|
996
|
+
const rootDir = path.resolve(projectDir);
|
|
997
|
+
const initialBuildConfig = loadTypeScriptBuildConfig(rootDir);
|
|
998
|
+
const distDir = initialBuildConfig.packageOutDirAbs;
|
|
999
|
+
const vendorDir = path.join(distDir, 'vendor');
|
|
1000
|
+
const dalilaRoot = resolveDalilaPackageRoot(rootDir);
|
|
1001
|
+
const indexHtmlPath = path.join(rootDir, 'index.html');
|
|
1002
|
+
|
|
1003
|
+
ensureFileExists(indexHtmlPath, 'index.html');
|
|
1004
|
+
const compiledOutput = copyCompiledOutputTree(initialBuildConfig);
|
|
1005
|
+
const buildConfig = compiledOutput.buildConfig;
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
fs.rmSync(vendorDir, { recursive: true, force: true });
|
|
1009
|
+
rewritePackagedModuleSpecifiers(buildConfig);
|
|
1010
|
+
copyDirectoryContents(path.join(dalilaRoot, 'dist'), path.join(vendorDir, 'dalila'));
|
|
1011
|
+
copyPackagedSourceAssets(rootDir, buildConfig);
|
|
1012
|
+
copyDirectoryContents(path.join(rootDir, 'public'), path.join(distDir, 'public'));
|
|
1013
|
+
copyTopLevelStaticDirs(rootDir, distDir, buildConfig);
|
|
1014
|
+
copyTopLevelStaticFiles(rootDir, distDir);
|
|
1015
|
+
|
|
1016
|
+
const copiedPackages = new Set();
|
|
1017
|
+
packageHtmlEntryPoints(rootDir, vendorDir, buildConfig, copiedPackages);
|
|
1018
|
+
|
|
1019
|
+
return {
|
|
1020
|
+
distDir,
|
|
1021
|
+
importEntries: {
|
|
1022
|
+
imports: {
|
|
1023
|
+
...buildUserProjectImportEntries(buildConfig),
|
|
1024
|
+
...buildDalilaImportEntries(rootDir),
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
} finally {
|
|
1029
|
+
compiledOutput.cleanup();
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1034
|
+
|
|
1035
|
+
if (isMain) {
|
|
1036
|
+
buildProject().catch((error) => {
|
|
1037
|
+
console.error('[Dalila] build packaging failed:', error);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
});
|
|
1040
|
+
}
|
package/template/index.html
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<div id="app" d-loading></div>
|
|
11
11
|
|
|
12
|
+
<script type="importmap">
|
|
13
|
+
{
|
|
14
|
+
"imports": {
|
|
15
|
+
"dompurify": "/node_modules/dompurify/dist/purify.es.mjs"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
12
19
|
<script type="module" src="/src/main.ts"></script>
|
|
13
20
|
</body>
|
|
14
21
|
</html>
|
package/template/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"dev": "node dev.mjs",
|
|
8
8
|
"routes": "dalila routes generate",
|
|
9
9
|
"routes:watch": "dalila routes watch",
|
|
10
|
-
"build": "tsc",
|
|
10
|
+
"build": "dalila routes generate && tsc && node build.mjs",
|
|
11
11
|
"preview": "dalila-dev --dist",
|
|
12
12
|
"postinstall": "dalila routes generate"
|
|
13
13
|
},
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"node": ">=22.6.0"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"dalila": "^1.9.
|
|
18
|
+
"dalila": "^1.9.23",
|
|
19
|
+
"dompurify": "^3.2.7"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"typescript": "^5.7.3"
|
package/template/src/main.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { configure } from 'dalila/runtime';
|
|
1
3
|
import { createRouter } from 'dalila/router';
|
|
2
4
|
import { routes } from '../routes.generated.js';
|
|
3
5
|
import { routeManifest } from '../routes.generated.manifest.js';
|
|
4
6
|
|
|
7
|
+
configure({
|
|
8
|
+
sanitizeHtml: (html) => DOMPurify.sanitize(html, {
|
|
9
|
+
USE_PROFILES: { html: true },
|
|
10
|
+
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'style'],
|
|
11
|
+
FORBID_ATTR: ['srcdoc'],
|
|
12
|
+
ALLOW_UNKNOWN_PROTOCOLS: false,
|
|
13
|
+
RETURN_TRUSTED_TYPE: false,
|
|
14
|
+
}),
|
|
15
|
+
security: {
|
|
16
|
+
strict: true,
|
|
17
|
+
trustedTypes: true,
|
|
18
|
+
trustedTypesPolicyName: '__DALILA_TRUSTED_TYPES_POLICY__',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
5
22
|
const outlet = document.getElementById('app');
|
|
6
23
|
|
|
7
24
|
if (!outlet) {
|