create-dalila 1.2.13 → 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 CHANGED
@@ -5,7 +5,7 @@ Scaffold a new Dalila project with one command.
5
5
  ## Usage
6
6
 
7
7
  ```bash
8
- npm create dalila my-app
8
+ npm create dalila@latest my-app
9
9
  cd my-app
10
10
  npm install
11
11
  npm run dev
@@ -13,20 +13,32 @@ npm run dev
13
13
 
14
14
  Open http://localhost:4242 to see your app.
15
15
 
16
+ ## Requirements
17
+
18
+ - Node.js `>=22.6.0`
19
+
16
20
  ## What's Included
17
21
 
18
- - Counter example with signals and computed values
19
- - Hot reload development server + route generation watcher
22
+ - File-based router starter (`src/app`)
23
+ - Dev server + route generation watcher
20
24
  - TypeScript support out of the box
21
25
  - Minimal CSS styling
26
+ - `dompurify` installed and wired into the runtime bootstrap
27
+ - Trusted Types enabled in the starter runtime config
22
28
 
23
29
  ## Project Structure
24
30
 
25
31
  ```
26
32
  my-app/
27
- ├── index.html # Main HTML file
33
+ ├── build.mjs # Packages a standalone dist/ preview build
34
+ ├── dev.mjs # Runs route watcher + dev server
35
+ ├── index.html # App shell
28
36
  ├── src/
29
- │ ├── main.ts # Application entry point
37
+ │ ├── app/
38
+ │ │ ├── layout.html
39
+ │ │ ├── page.html
40
+ │ │ └── page.ts
41
+ │ ├── main.ts # Router bootstrap + runtime security defaults
30
42
  │ └── style.css # Styles
31
43
  ├── package.json
32
44
  └── tsconfig.json
@@ -35,8 +47,11 @@ my-app/
35
47
  ## Scripts
36
48
 
37
49
  - `npm run dev` - Start dev server and route watcher
38
- - `npm run build` - Compile TypeScript
50
+ - `npm run routes` - Generate route files once
51
+ - `npm run routes:watch` - Watch route files and regenerate outputs
52
+ - `npm run build` - Generate routes, compile TypeScript, and package a standalone `dist/`
53
+ - `npm run preview` - Serve the built `dist/` output locally
39
54
 
40
55
  ## Learn More
41
56
 
42
- - [Dalila Documentation](https://github.com/evertonccarvalho/dalila)
57
+ - [Dalila Documentation](https://github.com/evertondsvieira/dalila)
package/index.js CHANGED
@@ -15,6 +15,7 @@ const green = (text) => `\x1b[32m${text}\x1b[0m`;
15
15
  const cyan = (text) => `\x1b[36m${text}\x1b[0m`;
16
16
  const yellow = (text) => `\x1b[33m${text}\x1b[0m`;
17
17
  const bold = (text) => `\x1b[1m${text}\x1b[0m`;
18
+ const MIN_NODE = { major: 22, minor: 6, patch: 0 };
18
19
 
19
20
  function printHelp() {
20
21
  console.log(`
@@ -34,6 +35,80 @@ ${bold('Options:')}
34
35
  `);
35
36
  }
36
37
 
38
+ function compareVersions(a, b) {
39
+ if (a.major !== b.major) return a.major - b.major;
40
+ if (a.minor !== b.minor) return a.minor - b.minor;
41
+ return a.patch - b.patch;
42
+ }
43
+
44
+ function parseNodeVersion(version) {
45
+ const [major = '0', minor = '0', patch = '0'] = version.split('.');
46
+ return {
47
+ major: Number.parseInt(major, 10) || 0,
48
+ minor: Number.parseInt(minor, 10) || 0,
49
+ patch: Number.parseInt(patch, 10) || 0,
50
+ };
51
+ }
52
+
53
+ function ensureSupportedNode() {
54
+ const current = parseNodeVersion(process.versions.node);
55
+ if (compareVersions(current, MIN_NODE) >= 0) return;
56
+
57
+ console.error(
58
+ `${yellow('Error:')} Node.js ${MIN_NODE.major}.${MIN_NODE.minor}.${MIN_NODE.patch}+ is required to create and run Dalila apps.\n`
59
+ );
60
+ console.error(`Current version: ${process.versions.node}`);
61
+ console.error('Please upgrade Node.js and try again.\n');
62
+ process.exit(1);
63
+ }
64
+
65
+ function suggestPackageName(input) {
66
+ return (input || 'my-app')
67
+ .trim()
68
+ .toLowerCase()
69
+ .replace(/\s+/g, '-')
70
+ .replace(/[^a-z0-9._~-]/g, '-')
71
+ .replace(/-+/g, '-')
72
+ .replace(/^[._-]+/, '')
73
+ .replace(/[._-]+$/, '') || 'my-app';
74
+ }
75
+
76
+ function validateProjectName(name) {
77
+ const trimmed = name.trim();
78
+ const errors = [];
79
+
80
+ if (trimmed.length === 0) {
81
+ errors.push('Project name cannot be empty.');
82
+ }
83
+ if (trimmed !== name) {
84
+ errors.push('Project name cannot start or end with spaces.');
85
+ }
86
+ if (trimmed.length > 214) {
87
+ errors.push('Project name must be 214 characters or fewer.');
88
+ }
89
+ if (trimmed.includes('/')) {
90
+ errors.push('Use an unscoped package name (e.g. "my-app").');
91
+ }
92
+ if (/[A-Z]/.test(trimmed)) {
93
+ errors.push('Project name must be lowercase.');
94
+ }
95
+ if (trimmed.startsWith('.') || trimmed.startsWith('_')) {
96
+ errors.push('Project name cannot start with "." or "_".');
97
+ }
98
+ if (!/^[a-z0-9][a-z0-9._~-]*$/.test(trimmed)) {
99
+ errors.push('Use only lowercase letters, numbers, ".", "_", "~", and "-".');
100
+ }
101
+ if (trimmed === 'node_modules' || trimmed === 'favicon.ico') {
102
+ errors.push(`"${trimmed}" is not a valid package name.`);
103
+ }
104
+
105
+ return {
106
+ valid: errors.length === 0,
107
+ errors,
108
+ suggested: suggestPackageName(trimmed),
109
+ };
110
+ }
111
+
37
112
  function copyDir(src, dest) {
38
113
  fs.mkdirSync(dest, { recursive: true });
39
114
 
@@ -56,7 +131,19 @@ function updatePackageJson(projectPath, projectName) {
56
131
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
57
132
  }
58
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
+
59
144
  function main() {
145
+ ensureSupportedNode();
146
+
60
147
  // Handle flags
61
148
  if (args.includes('-h') || args.includes('--help')) {
62
149
  printHelp();
@@ -78,6 +165,17 @@ function main() {
78
165
  process.exit(1);
79
166
  }
80
167
 
168
+ const validation = validateProjectName(projectName);
169
+ if (!validation.valid) {
170
+ console.error(`${yellow('Error:')} Invalid project name "${projectName}".\n`);
171
+ for (const error of validation.errors) {
172
+ console.error(`- ${error}`);
173
+ }
174
+ console.error('\nTry something like:');
175
+ console.error(` npm create dalila ${cyan(validation.suggested)}\n`);
176
+ process.exit(1);
177
+ }
178
+
81
179
  // Check if directory exists
82
180
  const projectPath = path.resolve(process.cwd(), projectName);
83
181
 
@@ -96,6 +194,7 @@ function main() {
96
194
 
97
195
  // Update package.json with project name
98
196
  updatePackageJson(projectPath, projectName);
197
+ updateTemplatePlaceholders(projectPath, projectName);
99
198
 
100
199
  // Success message
101
200
  console.log(`${green('Done!')} Created ${bold(projectName)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dalila",
3
- "version": "1.2.13",
3
+ "version": "1.2.15",
4
4
  "description": "Create Dalila apps with one command",
5
5
  "bin": {
6
6
  "create-dalila": "index.js"
@@ -23,6 +23,9 @@
23
23
  ],
24
24
  "author": "Everton Da Silva Vieira",
25
25
  "license": "MIT",
26
+ "engines": {
27
+ "node": ">=22.6.0"
28
+ },
26
29
  "repository": {
27
30
  "type": "git",
28
31
  "url": "https://github.com/evertondsvieira/dalila.git"
@@ -0,0 +1 @@
1
+ 22
@@ -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
+ }
@@ -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>
@@ -7,12 +7,16 @@
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
  },
14
+ "engines": {
15
+ "node": ">=22.6.0"
16
+ },
14
17
  "dependencies": {
15
- "dalila": "latest"
18
+ "dalila": "^1.9.23",
19
+ "dompurify": "^3.2.7"
16
20
  },
17
21
  "devDependencies": {
18
22
  "typescript": "^5.7.3"
@@ -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) {