dalila 1.10.1 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/persist.js +18 -5
- package/dist/runtime/html-sinks.js +171 -13
- package/package.json +1 -1
- package/scripts/dev-server.cjs +378 -51
- package/src/components/ui/side-bar/side-bar.css +57 -1
package/dist/core/persist.js
CHANGED
|
@@ -473,11 +473,24 @@ export function clearPersisted(name, storage = safeDefaultStorage() ?? {}) {
|
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
function escapeInlineScriptContent(script) {
|
|
476
|
-
return script
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
476
|
+
return script.replace(/--!>|-->|[<>\u2028\u2029]/g, (match) => {
|
|
477
|
+
switch (match) {
|
|
478
|
+
case '--!>':
|
|
479
|
+
return '--!\\u003E';
|
|
480
|
+
case '-->':
|
|
481
|
+
return '--\\u003E';
|
|
482
|
+
case '<':
|
|
483
|
+
return '\\u003C';
|
|
484
|
+
case '>':
|
|
485
|
+
return '\\u003E';
|
|
486
|
+
case '\u2028':
|
|
487
|
+
return '\\u2028';
|
|
488
|
+
case '\u2029':
|
|
489
|
+
return '\\u2029';
|
|
490
|
+
default:
|
|
491
|
+
return match;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
481
494
|
}
|
|
482
495
|
/**
|
|
483
496
|
* Generate a minimal inline script to prevent FOUC.
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
const TRUSTED_POLICY_CACHE_KEY = Symbol.for('dalila.runtime.trustedTypesPolicies');
|
|
2
2
|
const TRUSTED_POLICY_PARSE_SUFFIX = '--dalila-parse';
|
|
3
|
-
const EXECUTABLE_HTML_SCRIPT_PATTERN = /<script[\s/>]/i;
|
|
4
3
|
const EXECUTABLE_HTML_EVENT_ATTR_PATTERN = /<[^>]+\son[a-z0-9:_-]+\s*=/i;
|
|
5
|
-
const
|
|
4
|
+
const EXECUTABLE_HTML_URL_ATTR_NAMES = new Set([
|
|
5
|
+
'href',
|
|
6
|
+
'src',
|
|
7
|
+
'xlink:href',
|
|
8
|
+
'formaction',
|
|
9
|
+
'action',
|
|
10
|
+
'poster',
|
|
11
|
+
]);
|
|
6
12
|
const EXECUTABLE_DATA_URL_PATTERN = /^data:(?:text\/html|application\/xhtml\+xml|image\/svg\+xml)\b/i;
|
|
7
13
|
function getTrustedPolicyCache() {
|
|
8
14
|
const host = globalThis;
|
|
@@ -17,25 +23,177 @@ const trustedPolicyCache = getTrustedPolicyCache();
|
|
|
17
23
|
function normalizeHtmlUrlAttrValue(value) {
|
|
18
24
|
return value.replace(/[\u0000-\u0020\u007f]+/g, '').toLowerCase();
|
|
19
25
|
}
|
|
26
|
+
function isHtmlWhitespaceCode(code) {
|
|
27
|
+
return code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d;
|
|
28
|
+
}
|
|
29
|
+
function isHtmlAttributeNameChar(code) {
|
|
30
|
+
return !Number.isNaN(code)
|
|
31
|
+
&& code !== 0x20
|
|
32
|
+
&& code !== 0x09
|
|
33
|
+
&& code !== 0x0a
|
|
34
|
+
&& code !== 0x0c
|
|
35
|
+
&& code !== 0x0d
|
|
36
|
+
&& code !== 0x22
|
|
37
|
+
&& code !== 0x27
|
|
38
|
+
&& code !== 0x2f
|
|
39
|
+
&& code !== 0x3c
|
|
40
|
+
&& code !== 0x3d
|
|
41
|
+
&& code !== 0x3e
|
|
42
|
+
&& code !== 0x60;
|
|
43
|
+
}
|
|
44
|
+
function isTagBoundaryChar(char) {
|
|
45
|
+
return !char || /[\s/>]/.test(char);
|
|
46
|
+
}
|
|
47
|
+
function getPreviousNonWhitespaceChar(value, end, start = 0) {
|
|
48
|
+
for (let index = end - 1; index >= start; index -= 1) {
|
|
49
|
+
if (!isHtmlWhitespaceCode(value.charCodeAt(index))) {
|
|
50
|
+
return value[index];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function isHtmlTagStartChar(char) {
|
|
56
|
+
return !!char && /[A-Za-z/!?]/.test(char);
|
|
57
|
+
}
|
|
58
|
+
function findTagLikeStart(value, start, end = value.length) {
|
|
59
|
+
let index = value.indexOf('<', start);
|
|
60
|
+
while (index !== -1 && index < end) {
|
|
61
|
+
if (isHtmlTagStartChar(value[index + 1])) {
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
index = value.indexOf('<', index + 1);
|
|
65
|
+
}
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
function hasExecutableHtmlScriptTag(value) {
|
|
69
|
+
const lower = value.toLowerCase();
|
|
70
|
+
let searchIndex = 0;
|
|
71
|
+
while (searchIndex < lower.length) {
|
|
72
|
+
const index = lower.indexOf('<script', searchIndex);
|
|
73
|
+
if (index === -1)
|
|
74
|
+
return false;
|
|
75
|
+
if (isTagBoundaryChar(lower[index + 7])) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
searchIndex = index + 7;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function hasExecutableProtocol(value) {
|
|
83
|
+
return value.startsWith('javascript:')
|
|
84
|
+
|| value.startsWith('vbscript:')
|
|
85
|
+
|| EXECUTABLE_DATA_URL_PATTERN.test(value);
|
|
86
|
+
}
|
|
20
87
|
function hasExecutableHtmlUrlAttribute(value) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
88
|
+
let index = 0;
|
|
89
|
+
while (index < value.length) {
|
|
90
|
+
const tagStart = value.indexOf('<', index);
|
|
91
|
+
if (tagStart === -1)
|
|
92
|
+
return false;
|
|
93
|
+
let cursor = tagStart + 1;
|
|
94
|
+
const firstCode = value.charCodeAt(cursor);
|
|
95
|
+
if (Number.isNaN(firstCode)
|
|
96
|
+
|| value[cursor] === '/'
|
|
97
|
+
|| value[cursor] === '!'
|
|
98
|
+
|| value[cursor] === '?') {
|
|
99
|
+
index = cursor;
|
|
27
100
|
continue;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
101
|
+
}
|
|
102
|
+
while (cursor < value.length && !isHtmlWhitespaceCode(value.charCodeAt(cursor)) && value[cursor] !== '>') {
|
|
103
|
+
cursor += 1;
|
|
104
|
+
}
|
|
105
|
+
while (cursor < value.length && value[cursor] !== '>') {
|
|
106
|
+
while (cursor < value.length && isHtmlWhitespaceCode(value.charCodeAt(cursor))) {
|
|
107
|
+
cursor += 1;
|
|
108
|
+
}
|
|
109
|
+
if (cursor >= value.length || value[cursor] === '>') {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (value[cursor] === '/') {
|
|
113
|
+
cursor += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const nameStart = cursor;
|
|
117
|
+
while (cursor < value.length && isHtmlAttributeNameChar(value.charCodeAt(cursor))) {
|
|
118
|
+
cursor += 1;
|
|
119
|
+
}
|
|
120
|
+
if (cursor === nameStart) {
|
|
121
|
+
cursor += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const attrName = value.slice(nameStart, cursor).toLowerCase();
|
|
125
|
+
while (cursor < value.length && isHtmlWhitespaceCode(value.charCodeAt(cursor))) {
|
|
126
|
+
cursor += 1;
|
|
127
|
+
}
|
|
128
|
+
if (value[cursor] !== '=') {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
cursor += 1;
|
|
132
|
+
while (cursor < value.length && isHtmlWhitespaceCode(value.charCodeAt(cursor))) {
|
|
133
|
+
cursor += 1;
|
|
134
|
+
}
|
|
135
|
+
if (cursor >= value.length) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
let attrValue = '';
|
|
139
|
+
let recoveryTagIndex = -1;
|
|
140
|
+
let unterminatedQuotedValue = false;
|
|
141
|
+
const quote = value[cursor];
|
|
142
|
+
if (quote === '"' || quote === '\'') {
|
|
143
|
+
cursor += 1;
|
|
144
|
+
const valueStart = cursor;
|
|
145
|
+
const closingQuoteIndex = value.indexOf(quote, valueStart);
|
|
146
|
+
const quotedValueEnd = closingQuoteIndex === -1 ? value.length : closingQuoteIndex;
|
|
147
|
+
recoveryTagIndex = findTagLikeStart(value, valueStart, quotedValueEnd);
|
|
148
|
+
if (closingQuoteIndex === -1) {
|
|
149
|
+
unterminatedQuotedValue = true;
|
|
150
|
+
const valueEnd = recoveryTagIndex === -1 ? value.length : recoveryTagIndex;
|
|
151
|
+
attrValue = value.slice(valueStart, valueEnd);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const hasSuspiciousQuotedRestart = recoveryTagIndex !== -1
|
|
155
|
+
&& getPreviousNonWhitespaceChar(value, closingQuoteIndex, valueStart) === '=';
|
|
156
|
+
if (hasSuspiciousQuotedRestart) {
|
|
157
|
+
unterminatedQuotedValue = true;
|
|
158
|
+
attrValue = value.slice(valueStart, recoveryTagIndex);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
cursor = closingQuoteIndex;
|
|
162
|
+
attrValue = value.slice(valueStart, cursor);
|
|
163
|
+
cursor += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const valueStart = cursor;
|
|
169
|
+
while (cursor < value.length
|
|
170
|
+
&& !isHtmlWhitespaceCode(value.charCodeAt(cursor))
|
|
171
|
+
&& value[cursor] !== '>') {
|
|
172
|
+
cursor += 1;
|
|
173
|
+
}
|
|
174
|
+
attrValue = value.slice(valueStart, cursor);
|
|
175
|
+
}
|
|
176
|
+
if (EXECUTABLE_HTML_URL_ATTR_NAMES.has(attrName)) {
|
|
177
|
+
const normalized = normalizeHtmlUrlAttrValue(attrValue);
|
|
178
|
+
if (normalized && hasExecutableProtocol(normalized)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (unterminatedQuotedValue && recoveryTagIndex !== -1) {
|
|
183
|
+
return hasExecutableHtmlUrlAttribute(value.slice(recoveryTagIndex));
|
|
184
|
+
}
|
|
185
|
+
if (unterminatedQuotedValue) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
index = cursor + 1;
|
|
32
190
|
}
|
|
33
191
|
return false;
|
|
34
192
|
}
|
|
35
193
|
export function hasExecutableHtmlSinkPattern(value) {
|
|
36
194
|
if (!value)
|
|
37
195
|
return false;
|
|
38
|
-
return
|
|
196
|
+
return hasExecutableHtmlScriptTag(value)
|
|
39
197
|
|| EXECUTABLE_HTML_EVENT_ATTR_PATTERN.test(value)
|
|
40
198
|
|| hasExecutableHtmlUrlAttribute(value);
|
|
41
199
|
}
|
package/package.json
CHANGED
package/scripts/dev-server.cjs
CHANGED
|
@@ -27,6 +27,7 @@ function resolveServerConfig(argv = process.argv.slice(2), cwd = process.cwd())
|
|
|
27
27
|
const serverConfig = resolveServerConfig();
|
|
28
28
|
const projectDir = serverConfig.projectDir;
|
|
29
29
|
const rootDir = serverConfig.rootDir;
|
|
30
|
+
const rootDirAbs = fs.existsSync(rootDir) ? fs.realpathSync(rootDir) : path.resolve(rootDir);
|
|
30
31
|
const distMode = serverConfig.distMode;
|
|
31
32
|
const isDalilaRepo = serverConfig.isDalilaRepo;
|
|
32
33
|
const defaultEntry = serverConfig.defaultEntry;
|
|
@@ -107,6 +108,313 @@ function send(res, status, body, headers = {}) {
|
|
|
107
108
|
res.end(body);
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
const FORBIDDEN_PATH_ERROR_CODE = 'DALILA_FORBIDDEN_PATH';
|
|
112
|
+
|
|
113
|
+
function createForbiddenPathError() {
|
|
114
|
+
const error = new Error('Forbidden');
|
|
115
|
+
error.code = FORBIDDEN_PATH_ERROR_CODE;
|
|
116
|
+
return error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isPathInsideRoot(candidatePath) {
|
|
120
|
+
const normalizedPath = path.resolve(candidatePath);
|
|
121
|
+
const relativePath = path.relative(rootDirAbs, normalizedPath);
|
|
122
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeServedPath(candidatePath) {
|
|
126
|
+
if (typeof candidatePath !== 'string' || candidatePath.length === 0) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const normalizedPath = path.resolve(candidatePath);
|
|
131
|
+
return isPathInsideRoot(normalizedPath) ? normalizedPath : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function statServedPath(targetPath) {
|
|
135
|
+
const safePath = normalizeServedPath(targetPath);
|
|
136
|
+
if (!safePath) {
|
|
137
|
+
throw createForbiddenPathError();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
safePath,
|
|
142
|
+
stat: fs.statSync(safePath),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function existsServedPath(targetPath) {
|
|
147
|
+
const safePath = normalizeServedPath(targetPath);
|
|
148
|
+
return safePath ? fs.existsSync(safePath) : false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readServedFile(targetPath, encoding, callback) {
|
|
152
|
+
const safePath = normalizeServedPath(targetPath);
|
|
153
|
+
if (!safePath) {
|
|
154
|
+
queueMicrotask(() => callback(createForbiddenPathError()));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.readFile(safePath, encoding, callback);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function replaceServedPathExtension(targetPath, fromExtension, toExtension) {
|
|
162
|
+
const safePath = normalizeServedPath(targetPath);
|
|
163
|
+
if (!safePath || !safePath.endsWith(fromExtension)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return normalizeServedPath(`${safePath.slice(0, -fromExtension.length)}${toExtension}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function appendServedPathExtension(targetPath, extension) {
|
|
171
|
+
const safePath = normalizeServedPath(targetPath);
|
|
172
|
+
if (!safePath) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return normalizeServedPath(`${safePath}${extension}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function joinServedPath(targetPath, childPath) {
|
|
180
|
+
const safePath = normalizeServedPath(targetPath);
|
|
181
|
+
if (!safePath) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return normalizeServedPath(path.join(safePath, childPath));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function escapeInlineScriptContent(script) {
|
|
189
|
+
return script.replace(/--!>|-->|[<>\u2028\u2029]/g, (match) => {
|
|
190
|
+
switch (match) {
|
|
191
|
+
case '--!>':
|
|
192
|
+
return '--!\\u003E';
|
|
193
|
+
case '-->':
|
|
194
|
+
return '--\\u003E';
|
|
195
|
+
case '<':
|
|
196
|
+
return '\\u003C';
|
|
197
|
+
case '>':
|
|
198
|
+
return '\\u003E';
|
|
199
|
+
case '\u2028':
|
|
200
|
+
return '\\u2028';
|
|
201
|
+
case '\u2029':
|
|
202
|
+
return '\\u2029';
|
|
203
|
+
default:
|
|
204
|
+
return match;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stringifyInlineScriptPayload(value, indent = 0) {
|
|
210
|
+
const json = escapeInlineScriptContent(JSON.stringify(value, null, 2));
|
|
211
|
+
if (indent <= 0) {
|
|
212
|
+
return json;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const padding = ' '.repeat(indent);
|
|
216
|
+
return json
|
|
217
|
+
.split('\n')
|
|
218
|
+
.map((line) => `${padding}${line}`)
|
|
219
|
+
.join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizePreloadStorageType(storageType) {
|
|
223
|
+
return storageType === 'sessionStorage' ? 'sessionStorage' : 'localStorage';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isHtmlWhitespaceChar(char) {
|
|
227
|
+
return char === ' ' || char === '\n' || char === '\r' || char === '\t' || char === '\f';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isHtmlTagBoundary(char) {
|
|
231
|
+
return !char || isHtmlWhitespaceChar(char) || char === '>' || char === '/';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findHtmlTagEnd(html, startIndex) {
|
|
235
|
+
let quote = null;
|
|
236
|
+
|
|
237
|
+
for (let index = startIndex; index < html.length; index += 1) {
|
|
238
|
+
const char = html[index];
|
|
239
|
+
if (quote) {
|
|
240
|
+
if (char === quote) {
|
|
241
|
+
quote = null;
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (char === '"' || char === '\'') {
|
|
247
|
+
quote = char;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (char === '>') {
|
|
252
|
+
return index;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return -1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function findScriptCloseTagStart(lower, searchIndex) {
|
|
260
|
+
let index = lower.indexOf('</script', searchIndex);
|
|
261
|
+
|
|
262
|
+
while (index !== -1) {
|
|
263
|
+
if (isHtmlTagBoundary(lower[index + 8])) {
|
|
264
|
+
return index;
|
|
265
|
+
}
|
|
266
|
+
index = lower.indexOf('</script', index + 8);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return -1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getHtmlAttributeValue(attributesSource, attributeName) {
|
|
273
|
+
const name = attributeName.toLowerCase();
|
|
274
|
+
let index = 0;
|
|
275
|
+
|
|
276
|
+
while (index < attributesSource.length) {
|
|
277
|
+
while (index < attributesSource.length && isHtmlWhitespaceChar(attributesSource[index])) {
|
|
278
|
+
index += 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (index >= attributesSource.length) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (attributesSource[index] === '/') {
|
|
286
|
+
index += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const nameStart = index;
|
|
291
|
+
while (
|
|
292
|
+
index < attributesSource.length
|
|
293
|
+
&& !isHtmlWhitespaceChar(attributesSource[index])
|
|
294
|
+
&& !['=', '>', '"', '\'', '`'].includes(attributesSource[index])
|
|
295
|
+
) {
|
|
296
|
+
index += 1;
|
|
297
|
+
}
|
|
298
|
+
if (index === nameStart) {
|
|
299
|
+
index += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const currentName = attributesSource.slice(nameStart, index).toLowerCase();
|
|
304
|
+
|
|
305
|
+
while (index < attributesSource.length && isHtmlWhitespaceChar(attributesSource[index])) {
|
|
306
|
+
index += 1;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (attributesSource[index] !== '=') {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
index += 1;
|
|
313
|
+
|
|
314
|
+
while (index < attributesSource.length && isHtmlWhitespaceChar(attributesSource[index])) {
|
|
315
|
+
index += 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (index >= attributesSource.length) {
|
|
319
|
+
return currentName === name ? '' : null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let value = '';
|
|
323
|
+
const quote = attributesSource[index];
|
|
324
|
+
if (quote === '"' || quote === '\'') {
|
|
325
|
+
index += 1;
|
|
326
|
+
const valueStart = index;
|
|
327
|
+
while (index < attributesSource.length && attributesSource[index] !== quote) {
|
|
328
|
+
index += 1;
|
|
329
|
+
}
|
|
330
|
+
value = attributesSource.slice(valueStart, index);
|
|
331
|
+
if (index < attributesSource.length) {
|
|
332
|
+
index += 1;
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
const valueStart = index;
|
|
336
|
+
while (
|
|
337
|
+
index < attributesSource.length
|
|
338
|
+
&& !isHtmlWhitespaceChar(attributesSource[index])
|
|
339
|
+
&& attributesSource[index] !== '>'
|
|
340
|
+
) {
|
|
341
|
+
index += 1;
|
|
342
|
+
}
|
|
343
|
+
value = attributesSource.slice(valueStart, index);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (currentName === name) {
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function forEachHtmlScriptElement(html, visitor) {
|
|
355
|
+
const lower = html.toLowerCase();
|
|
356
|
+
let searchIndex = 0;
|
|
357
|
+
|
|
358
|
+
while (searchIndex < html.length) {
|
|
359
|
+
const openStart = lower.indexOf('<script', searchIndex);
|
|
360
|
+
if (openStart === -1) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (!isHtmlTagBoundary(lower[openStart + 7])) {
|
|
364
|
+
searchIndex = openStart + 7;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const openEnd = findHtmlTagEnd(html, openStart);
|
|
369
|
+
if (openEnd === -1) {
|
|
370
|
+
searchIndex = openStart + 7;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const closeStart = findScriptCloseTagStart(lower, openEnd + 1);
|
|
375
|
+
if (closeStart === -1) {
|
|
376
|
+
searchIndex = openStart + 7;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const closeEnd = findHtmlTagEnd(html, closeStart);
|
|
381
|
+
if (closeEnd === -1) {
|
|
382
|
+
searchIndex = closeStart + 8;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const element = {
|
|
387
|
+
attributesSource: html.slice(openStart + 7, openEnd),
|
|
388
|
+
content: html.slice(openEnd + 1, closeStart),
|
|
389
|
+
fullMatch: html.slice(openStart, closeEnd + 1),
|
|
390
|
+
start: openStart,
|
|
391
|
+
end: closeEnd + 1,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (visitor(element) === false) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
searchIndex = closeEnd + 1;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function findFirstHtmlScriptElementByType(html, type) {
|
|
403
|
+
let found = null;
|
|
404
|
+
|
|
405
|
+
forEachHtmlScriptElement(html, (element) => {
|
|
406
|
+
const scriptType = getHtmlAttributeValue(element.attributesSource, 'type');
|
|
407
|
+
if ((scriptType ?? '').toLowerCase() !== type) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
found = element;
|
|
412
|
+
return false;
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
return found;
|
|
416
|
+
}
|
|
417
|
+
|
|
110
418
|
/**
|
|
111
419
|
* Secure path resolution:
|
|
112
420
|
* - Strip leading slashes to treat URL as relative
|
|
@@ -126,18 +434,8 @@ function resolvePath(urlPath) {
|
|
|
126
434
|
const relativePath = decoded.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
127
435
|
|
|
128
436
|
// Resolve to absolute path
|
|
129
|
-
const fsPath = path.resolve(
|
|
130
|
-
|
|
131
|
-
// Normalize for comparison (handles .., ., etc.)
|
|
132
|
-
const normalizedPath = path.normalize(fsPath);
|
|
133
|
-
|
|
134
|
-
// Security check: must be within rootDir
|
|
135
|
-
// Use startsWith with path.sep to prevent /root matching /rootkit
|
|
136
|
-
if (!normalizedPath.startsWith(rootDir + path.sep) && normalizedPath !== rootDir) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return normalizedPath;
|
|
437
|
+
const fsPath = path.resolve(rootDirAbs, relativePath);
|
|
438
|
+
return normalizeServedPath(fsPath);
|
|
141
439
|
}
|
|
142
440
|
|
|
143
441
|
function safeDecodeUrlPath(url) {
|
|
@@ -327,18 +625,17 @@ function createImportMapEntries(dalilaPath, sourceDirPath = '/src/') {
|
|
|
327
625
|
}
|
|
328
626
|
|
|
329
627
|
function createImportMapScript(dalilaPath, sourceDirPath = '/src/') {
|
|
330
|
-
const payload =
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
628
|
+
const payload = stringifyInlineScriptPayload(
|
|
629
|
+
{ imports: createImportMapEntries(dalilaPath, sourceDirPath) },
|
|
630
|
+
4
|
|
631
|
+
);
|
|
334
632
|
|
|
335
633
|
return ` <script type="importmap">\n${payload}\n </script>`;
|
|
336
634
|
}
|
|
337
635
|
|
|
338
636
|
function mergeImportMapIntoHtml(html, dalilaPath, sourceDirPath = '/src/') {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
if (!match) {
|
|
637
|
+
const importMapElement = findFirstHtmlScriptElementByType(html, 'importmap');
|
|
638
|
+
if (!importMapElement) {
|
|
342
639
|
return {
|
|
343
640
|
html,
|
|
344
641
|
merged: false,
|
|
@@ -346,7 +643,7 @@ function mergeImportMapIntoHtml(html, dalilaPath, sourceDirPath = '/src/') {
|
|
|
346
643
|
};
|
|
347
644
|
}
|
|
348
645
|
|
|
349
|
-
const existingPayload =
|
|
646
|
+
const existingPayload = importMapElement.content.trim() || '{}';
|
|
350
647
|
let importMap;
|
|
351
648
|
try {
|
|
352
649
|
importMap = JSON.parse(existingPayload);
|
|
@@ -368,14 +665,11 @@ function mergeImportMapIntoHtml(html, dalilaPath, sourceDirPath = '/src/') {
|
|
|
368
665
|
...existingImports,
|
|
369
666
|
},
|
|
370
667
|
};
|
|
371
|
-
const payload =
|
|
372
|
-
.split('\n')
|
|
373
|
-
.map(line => ` ${line}`)
|
|
374
|
-
.join('\n');
|
|
668
|
+
const payload = stringifyInlineScriptPayload(mergedImportMap, 4);
|
|
375
669
|
const script = ` <script type="importmap">\n${payload}\n </script>`;
|
|
376
670
|
|
|
377
671
|
return {
|
|
378
|
-
html: html.
|
|
672
|
+
html: `${html.slice(0, importMapElement.start)}${html.slice(importMapElement.end)}`,
|
|
379
673
|
merged: true,
|
|
380
674
|
script,
|
|
381
675
|
};
|
|
@@ -468,14 +762,15 @@ function findTypeScriptFiles(dir, files = []) {
|
|
|
468
762
|
* Generate inline preload script
|
|
469
763
|
*/
|
|
470
764
|
function generatePreloadScript(name, defaultValue, storageType = 'localStorage') {
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
765
|
+
const safeStorageType = normalizePreloadStorageType(storageType);
|
|
766
|
+
const payload = JSON.stringify({
|
|
767
|
+
key: name,
|
|
768
|
+
defaultValue,
|
|
769
|
+
storageType: safeStorageType,
|
|
770
|
+
});
|
|
771
|
+
const fallbackValue = JSON.stringify(defaultValue);
|
|
772
|
+
const script = `(function(){try{var p=${payload};var s=window[p.storageType];var v=s.getItem(p.key);document.documentElement.setAttribute('data-theme',v==null?p.defaultValue:JSON.parse(v))}catch(e){document.documentElement.setAttribute('data-theme',${fallbackValue})}})();`;
|
|
773
|
+
return escapeInlineScriptContent(script);
|
|
479
774
|
}
|
|
480
775
|
|
|
481
776
|
function renderPreloadScriptTags(baseDir) {
|
|
@@ -578,8 +873,8 @@ function addLoadingAttributes(html) {
|
|
|
578
873
|
// ============================================================================
|
|
579
874
|
// Binding Injection (for HTML files that need runtime bindings)
|
|
580
875
|
// ============================================================================
|
|
581
|
-
function injectBindings(html,
|
|
582
|
-
const
|
|
876
|
+
function injectBindings(html, options = {}) {
|
|
877
|
+
const isPlaygroundPage = options.isPlaygroundPage === true;
|
|
583
878
|
// Different paths for dalila repo vs user projects
|
|
584
879
|
const dalilaPath = isDalilaRepo ? '/dist' : '/node_modules/dalila/dist';
|
|
585
880
|
const sourceDirPath = buildProjectSourceDirPath(projectDir);
|
|
@@ -760,7 +1055,7 @@ function injectBindings(html, requestPath) {
|
|
|
760
1055
|
}
|
|
761
1056
|
|
|
762
1057
|
// Dalila repo: only inject import map for non-playground pages
|
|
763
|
-
if (
|
|
1058
|
+
if (!isPlaygroundPage) {
|
|
764
1059
|
return injectHeadFragments(html, [importMap], {
|
|
765
1060
|
beforeModule: true,
|
|
766
1061
|
beforeStyles: true,
|
|
@@ -1004,7 +1299,9 @@ const server = http.createServer((req, res) => {
|
|
|
1004
1299
|
|
|
1005
1300
|
let targetPath = fsPath;
|
|
1006
1301
|
try {
|
|
1007
|
-
const
|
|
1302
|
+
const target = statServedPath(targetPath);
|
|
1303
|
+
targetPath = target.safePath;
|
|
1304
|
+
const stat = target.stat;
|
|
1008
1305
|
if (stat.isDirectory()) {
|
|
1009
1306
|
// Redirect directory URLs without trailing slash to include it
|
|
1010
1307
|
if (!requestPath.endsWith('/')) {
|
|
@@ -1012,13 +1309,23 @@ const server = http.createServer((req, res) => {
|
|
|
1012
1309
|
res.end();
|
|
1013
1310
|
return;
|
|
1014
1311
|
}
|
|
1015
|
-
|
|
1312
|
+
const indexPath = joinServedPath(targetPath, 'index.html');
|
|
1313
|
+
if (!indexPath) {
|
|
1314
|
+
send(res, 403, 'Forbidden');
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
targetPath = indexPath;
|
|
1016
1318
|
}
|
|
1017
|
-
} catch {
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
if (err && err.code === FORBIDDEN_PATH_ERROR_CODE) {
|
|
1321
|
+
send(res, 403, 'Forbidden');
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1018
1325
|
// If .js file not found, try .ts alternative
|
|
1019
1326
|
if (targetPath.endsWith('.js')) {
|
|
1020
|
-
const tsPath = targetPath.
|
|
1021
|
-
if (
|
|
1327
|
+
const tsPath = replaceServedPathExtension(targetPath, '.js', '.ts');
|
|
1328
|
+
if (tsPath && existsServedPath(tsPath)) {
|
|
1022
1329
|
targetPath = tsPath;
|
|
1023
1330
|
} else {
|
|
1024
1331
|
send(res, 404, 'Not Found');
|
|
@@ -1026,11 +1333,11 @@ const server = http.createServer((req, res) => {
|
|
|
1026
1333
|
}
|
|
1027
1334
|
} else {
|
|
1028
1335
|
// Extensionless import — try .ts, then .js
|
|
1029
|
-
const tsPath = targetPath
|
|
1030
|
-
const jsPath = targetPath
|
|
1031
|
-
if (!path.extname(targetPath) && isScriptRequest &&
|
|
1336
|
+
const tsPath = appendServedPathExtension(targetPath, '.ts');
|
|
1337
|
+
const jsPath = appendServedPathExtension(targetPath, '.js');
|
|
1338
|
+
if (!path.extname(targetPath) && isScriptRequest && tsPath && existsServedPath(tsPath)) {
|
|
1032
1339
|
targetPath = tsPath;
|
|
1033
|
-
} else if (!path.extname(targetPath) && isScriptRequest &&
|
|
1340
|
+
} else if (!path.extname(targetPath) && isScriptRequest && jsPath && existsServedPath(jsPath)) {
|
|
1034
1341
|
targetPath = jsPath;
|
|
1035
1342
|
} else {
|
|
1036
1343
|
const spaFallback = resolveSpaFallbackPath(requestPath);
|
|
@@ -1052,8 +1359,12 @@ const server = http.createServer((req, res) => {
|
|
|
1052
1359
|
&& isScriptRequest
|
|
1053
1360
|
&& !isNavigationRequest;
|
|
1054
1361
|
if (isRawQuery || isHtmlModuleImport) {
|
|
1055
|
-
|
|
1362
|
+
readServedFile(targetPath, 'utf8', (err, source) => {
|
|
1056
1363
|
if (err) {
|
|
1364
|
+
if (err.code === FORBIDDEN_PATH_ERROR_CODE) {
|
|
1365
|
+
send(res, 403, 'Forbidden');
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1057
1368
|
send(res, err.code === 'ENOENT' ? 404 : 500, err.code === 'ENOENT' ? 'Not Found' : 'Error');
|
|
1058
1369
|
return;
|
|
1059
1370
|
}
|
|
@@ -1069,8 +1380,12 @@ const server = http.createServer((req, res) => {
|
|
|
1069
1380
|
|
|
1070
1381
|
// TypeScript transpilation (only if ts available)
|
|
1071
1382
|
if (targetPath.endsWith('.ts') && ts) {
|
|
1072
|
-
|
|
1383
|
+
readServedFile(targetPath, 'utf8', (err, source) => {
|
|
1073
1384
|
if (err) {
|
|
1385
|
+
if (err.code === FORBIDDEN_PATH_ERROR_CODE) {
|
|
1386
|
+
send(res, 403, 'Forbidden');
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1074
1389
|
if (err.code === 'ENOENT' || err.code === 'EISDIR') {
|
|
1075
1390
|
send(res, 404, 'Not Found');
|
|
1076
1391
|
return;
|
|
@@ -1104,15 +1419,23 @@ const server = http.createServer((req, res) => {
|
|
|
1104
1419
|
}
|
|
1105
1420
|
|
|
1106
1421
|
// Static file serving
|
|
1107
|
-
|
|
1422
|
+
readServedFile(targetPath, null, (err, data) => {
|
|
1108
1423
|
if (err) {
|
|
1424
|
+
if (err.code === FORBIDDEN_PATH_ERROR_CODE) {
|
|
1425
|
+
send(res, 403, 'Forbidden');
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1109
1428
|
if (err.code === 'ENOENT' || err.code === 'EISDIR') {
|
|
1110
1429
|
const spaFallback = resolveSpaFallbackPath(requestPath);
|
|
1111
1430
|
if (spaFallback) {
|
|
1112
1431
|
targetPath = spaFallback.fsPath;
|
|
1113
1432
|
resolvedRequestPath = spaFallback.requestPath;
|
|
1114
|
-
|
|
1433
|
+
readServedFile(targetPath, null, (fallbackErr, fallbackData) => {
|
|
1115
1434
|
if (fallbackErr) {
|
|
1435
|
+
if (fallbackErr.code === FORBIDDEN_PATH_ERROR_CODE) {
|
|
1436
|
+
send(res, 403, 'Forbidden');
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1116
1439
|
send(res, 404, 'Not Found');
|
|
1117
1440
|
return;
|
|
1118
1441
|
}
|
|
@@ -1125,7 +1448,9 @@ const server = http.createServer((req, res) => {
|
|
|
1125
1448
|
};
|
|
1126
1449
|
|
|
1127
1450
|
if (shouldInjectBindings(resolvedRequestPath, htmlSource)) {
|
|
1128
|
-
const html = injectBindings(htmlSource,
|
|
1451
|
+
const html = injectBindings(htmlSource, {
|
|
1452
|
+
isPlaygroundPage: normalizeHtmlRequestPath(resolvedRequestPath) === '/examples/playground/index.html',
|
|
1453
|
+
});
|
|
1129
1454
|
writeResponseHead(res, 200, headers);
|
|
1130
1455
|
res.end(html);
|
|
1131
1456
|
return;
|
|
@@ -1157,7 +1482,9 @@ const server = http.createServer((req, res) => {
|
|
|
1157
1482
|
if (ext === '.html') {
|
|
1158
1483
|
const htmlSource = data.toString('utf8');
|
|
1159
1484
|
if (shouldInjectBindings(resolvedRequestPath, htmlSource)) {
|
|
1160
|
-
const html = injectBindings(htmlSource,
|
|
1485
|
+
const html = injectBindings(htmlSource, {
|
|
1486
|
+
isPlaygroundPage: normalizeHtmlRequestPath(resolvedRequestPath) === '/examples/playground/index.html',
|
|
1487
|
+
});
|
|
1161
1488
|
writeResponseHead(res, 200, headers);
|
|
1162
1489
|
res.end(html);
|
|
1163
1490
|
return;
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
transition:
|
|
14
14
|
width var(--d-duration-slow) var(--d-ease),
|
|
15
15
|
max-width var(--d-duration-slow) var(--d-ease),
|
|
16
|
-
padding var(--d-duration) var(--d-ease)
|
|
16
|
+
padding var(--d-duration) var(--d-ease),
|
|
17
|
+
transform var(--d-duration-slow) var(--d-ease),
|
|
18
|
+
opacity var(--d-duration) var(--d-ease),
|
|
19
|
+
visibility var(--d-duration) var(--d-ease),
|
|
20
|
+
box-shadow var(--d-duration) var(--d-ease);
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
.d-side-bar-shell {
|
|
@@ -100,6 +104,24 @@
|
|
|
100
104
|
display: none;
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
.d-side-bar-backdrop {
|
|
108
|
+
position: fixed;
|
|
109
|
+
inset: 0;
|
|
110
|
+
display: block;
|
|
111
|
+
padding: 0;
|
|
112
|
+
border: 0;
|
|
113
|
+
background: var(--d-surface-overlay);
|
|
114
|
+
opacity: 0;
|
|
115
|
+
pointer-events: none;
|
|
116
|
+
transition: opacity var(--d-duration) var(--d-ease);
|
|
117
|
+
z-index: calc(var(--d-z-overlay) - 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.d-side-bar-backdrop.visible {
|
|
121
|
+
opacity: 1;
|
|
122
|
+
pointer-events: auto;
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
.d-side-bar-inner {
|
|
104
126
|
display: flex;
|
|
105
127
|
flex-direction: column;
|
|
@@ -412,3 +434,37 @@
|
|
|
412
434
|
background: linear-gradient(145deg, var(--d-primary-500), var(--d-accent-500));
|
|
413
435
|
color: #fff;
|
|
414
436
|
}
|
|
437
|
+
|
|
438
|
+
@media (max-width: 840px) {
|
|
439
|
+
.d-side-bar-mobile-shell {
|
|
440
|
+
display: block;
|
|
441
|
+
width: 100%;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.d-side-bar-mobile-shell .d-side-bar {
|
|
445
|
+
position: fixed;
|
|
446
|
+
inset: var(--d-space-4) auto var(--d-space-4) var(--d-space-4);
|
|
447
|
+
width: min(20rem, calc(100vw - (var(--d-space-4) * 2)));
|
|
448
|
+
max-width: calc(100vw - (var(--d-space-4) * 2));
|
|
449
|
+
min-height: auto;
|
|
450
|
+
max-height: calc(100dvh - (var(--d-space-4) * 2));
|
|
451
|
+
overflow-y: auto;
|
|
452
|
+
box-shadow: var(--d-shadow-lg);
|
|
453
|
+
transform: translateX(calc(-100% - var(--d-space-4)));
|
|
454
|
+
opacity: 0;
|
|
455
|
+
visibility: hidden;
|
|
456
|
+
pointer-events: none;
|
|
457
|
+
z-index: var(--d-z-overlay);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.d-side-bar-mobile-shell .d-side-bar.mobile-open {
|
|
461
|
+
transform: translateX(0);
|
|
462
|
+
opacity: 1;
|
|
463
|
+
visibility: visible;
|
|
464
|
+
pointer-events: auto;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.d-side-bar-mobile-shell .d-side-bar-rail {
|
|
468
|
+
display: none;
|
|
469
|
+
}
|
|
470
|
+
}
|