@lightningjs/blits 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/index.d.ts +13 -2
- package/package.json +1 -1
- package/src/announcer/announcer.js +1 -1
- package/src/announcer/speechSynthesis.js +1 -1
- package/src/component/base/router.js +136 -22
- package/src/components/RouterView.js +27 -7
- package/src/focus/focus.js +1 -0
- package/src/lib/reactivityguard/computedprops.js +235 -153
- package/src/router/router.js +381 -503
- package/src/router/utils.js +245 -0
|
@@ -100,176 +100,258 @@ const skipNonCode = (text, pos) => {
|
|
|
100
100
|
return p
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
103
|
+
// Skip a non-method computed member (`key: value`) and return the position after it.
|
|
104
|
+
const skipComputedMember = (text, pos) => {
|
|
105
|
+
const keyMatch = /^(\w+)\s*:\s*/.exec(text.substring(pos))
|
|
106
|
+
if (!keyMatch) return -1
|
|
107
|
+
|
|
108
|
+
let p = pos + keyMatch[0].length
|
|
109
|
+
p = skipNonCode(text, p)
|
|
110
|
+
if (p >= text.length - 1) return p
|
|
111
|
+
|
|
112
|
+
if (text.startsWith('function', p)) {
|
|
113
|
+
const open = text.indexOf('{', p)
|
|
114
|
+
if (open === -1) return -1
|
|
115
|
+
const close = findMatchingBrace(text, open)
|
|
116
|
+
return close === -1 ? -1 : close
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let depth = 0
|
|
120
|
+
let inString = false
|
|
121
|
+
let stringChar = ''
|
|
122
|
+
|
|
123
|
+
for (; p < text.length - 1; p++) {
|
|
124
|
+
const char = text[p]
|
|
125
|
+
|
|
126
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
127
|
+
inString = true
|
|
128
|
+
stringChar = char
|
|
129
|
+
} else if (inString && char === stringChar && text[p - 1] !== '\\') {
|
|
130
|
+
inString = false
|
|
131
|
+
} else if (!inString && char === '/' && text[p + 1] === '/') {
|
|
132
|
+
const nextNewline = text.indexOf('\n', p + 2)
|
|
133
|
+
p = nextNewline === -1 ? text.length - 2 : nextNewline
|
|
134
|
+
} else if (!inString && char === '/' && text[p + 1] === '*') {
|
|
135
|
+
const end = text.indexOf('*/', p + 2)
|
|
136
|
+
p = end === -1 ? text.length - 2 : end + 1
|
|
137
|
+
} else if (!inString) {
|
|
138
|
+
if (char === '{' || char === '(' || char === '[') depth++
|
|
139
|
+
else if (char === '}' || char === ')' || char === ']') depth--
|
|
140
|
+
else if (char === ',' && depth === 0) return p
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return p
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Find the opening `{` of the component config object after Blits.Component( / Application(
|
|
148
|
+
const findConfigBraceStart = (text, openParenPos) => {
|
|
149
|
+
let p = openParenPos + 1
|
|
150
|
+
p = skipNonCode(text, p)
|
|
151
|
+
if (p >= text.length) return -1
|
|
152
|
+
|
|
153
|
+
if (text[p] === '{') return p
|
|
154
|
+
|
|
155
|
+
if (text[p] === "'" || text[p] === '"' || text[p] === '`') {
|
|
156
|
+
const quote = text[p]
|
|
157
|
+
p++
|
|
158
|
+
while (p < text.length) {
|
|
159
|
+
if (text[p] === quote && text[p - 1] !== '\\') {
|
|
160
|
+
p++
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
p++
|
|
164
|
+
}
|
|
165
|
+
p = skipNonCode(text, p)
|
|
166
|
+
if (p < text.length && text[p] === '{') return p
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return -1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const processComputedBlock = (configObject) => {
|
|
107
173
|
const modifications = []
|
|
108
174
|
const commentText = ' /* auto-generated reactivity guard */ '
|
|
109
175
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!functionNameMatch) break
|
|
145
|
-
|
|
146
|
-
const funcName = functionNameMatch[1]
|
|
147
|
-
const funcNamePos = currentPos
|
|
148
|
-
|
|
149
|
-
// Find opening brace for this function
|
|
150
|
-
const afterSignaturePos = funcNamePos + functionNameMatch[0].length
|
|
151
|
-
const openingBracePos = computedObj.indexOf('{', afterSignaturePos)
|
|
152
|
-
if (openingBracePos === -1) break
|
|
153
|
-
|
|
154
|
-
// Find closing brace for individual computed function
|
|
155
|
-
const closingBracePos = findComputedFunctionEnd(computedObj, openingBracePos)
|
|
156
|
-
|
|
157
|
-
if (closingBracePos === -1) {
|
|
158
|
-
break
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Extract the entire function and its body
|
|
162
|
-
// This is better and more accurate than using regex to avoid nested braces
|
|
163
|
-
const functionStartPos = funcNamePos
|
|
164
|
-
const functionEndPos = closingBracePos
|
|
165
|
-
const fullFunction = computedObj.substring(functionStartPos, functionEndPos)
|
|
166
|
-
const functionBody = computedObj
|
|
167
|
-
.substring(openingBracePos + 1, closingBracePos - 1)
|
|
168
|
-
.trim()
|
|
169
|
-
|
|
170
|
-
computedProps.push({
|
|
171
|
-
name: funcName,
|
|
172
|
-
fullText: fullFunction,
|
|
173
|
-
body: functionBody,
|
|
174
|
-
startPos: functionStartPos,
|
|
175
|
-
endPos: functionEndPos,
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// Move past this function
|
|
179
|
-
currentPos = closingBracePos
|
|
176
|
+
const computedKeywordMatch = /computed\s*:\s*{/.exec(configObject)
|
|
177
|
+
|
|
178
|
+
if (computedKeywordMatch) {
|
|
179
|
+
// Find start position of the computed block
|
|
180
|
+
const computedStart = computedKeywordMatch.index + computedKeywordMatch[0].length - 1 // Position of the opening brace
|
|
181
|
+
|
|
182
|
+
// Find the matching closing brace for computed object in config
|
|
183
|
+
const computedEnd = findComputedObjectEnd(configObject, computedStart)
|
|
184
|
+
|
|
185
|
+
if (computedEnd !== -1) {
|
|
186
|
+
const computedObj = configObject.substring(computedStart, computedEnd)
|
|
187
|
+
const computedBlockStart = computedKeywordMatch.index
|
|
188
|
+
const computedBlockEnd = computedEnd
|
|
189
|
+
|
|
190
|
+
// Process each computed property
|
|
191
|
+
const computedProps = []
|
|
192
|
+
let currentPos = 1 // Start after the opening brace
|
|
193
|
+
|
|
194
|
+
// Process computed properties content
|
|
195
|
+
// intentionally not using regexes to extract functions and their content
|
|
196
|
+
// using cursor-based parsing gives better accuracy
|
|
197
|
+
while (currentPos < computedObj.length - 1) {
|
|
198
|
+
// -1 to exclude the closing brace
|
|
199
|
+
// Skip whitespace/comments so we don't match commented-out computed functions
|
|
200
|
+
currentPos = skipNonCode(computedObj, currentPos)
|
|
201
|
+
if (currentPos >= computedObj.length - 1) break
|
|
202
|
+
|
|
203
|
+
// Find the next function name (computed properties are `name() { ... }`)
|
|
204
|
+
const functionNameMatch = /^(\w+)\s*\(\)\s*/.exec(computedObj.substring(currentPos))
|
|
205
|
+
if (!functionNameMatch) {
|
|
206
|
+
const nextPos = skipComputedMember(computedObj, currentPos)
|
|
207
|
+
if (nextPos === -1) break
|
|
208
|
+
currentPos = nextPos
|
|
209
|
+
continue
|
|
180
210
|
}
|
|
181
211
|
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (thisRefs.size > 0) {
|
|
196
|
-
// Skip if already modified
|
|
197
|
-
if (
|
|
198
|
-
prop.body.includes(commentText.trim()) ||
|
|
199
|
-
Array.from(thisRefs).some((ref) => {
|
|
200
|
-
if (prop.body.startsWith(ref)) return true
|
|
201
|
-
// stripped && guard: body starts with the first segment, e.g. 'this.media &&'
|
|
202
|
-
const firstSegEnd = ref.indexOf('.', 5)
|
|
203
|
-
return firstSegEnd !== -1 && prop.body.startsWith(ref.substring(0, firstSegEnd))
|
|
204
|
-
})
|
|
205
|
-
) {
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// reactivity code
|
|
210
|
-
const refCode = Array.from(thisRefs)
|
|
211
|
-
.map((ref) => `${toSafeRef(ref)};`)
|
|
212
|
-
.join(' ')
|
|
213
|
-
|
|
214
|
-
// replacement with the same whitespace
|
|
215
|
-
const openBraceIndex = prop.fullText.indexOf('{')
|
|
216
|
-
|
|
217
|
-
if (openBraceIndex !== -1) {
|
|
218
|
-
// Extract indentation from the original function body
|
|
219
|
-
const indentMatch = prop.body.match(/^(\s+)/)
|
|
220
|
-
const indent = indentMatch ? indentMatch[1] + ' ' : ' ' // Default to 4 spaces if no indent found
|
|
221
|
-
|
|
222
|
-
const modifiedPropText =
|
|
223
|
-
prop.fullText.substring(0, openBraceIndex + 1) +
|
|
224
|
-
'\n' +
|
|
225
|
-
indent +
|
|
226
|
-
commentText +
|
|
227
|
-
'\n' +
|
|
228
|
-
indent +
|
|
229
|
-
refCode +
|
|
230
|
-
'\n' +
|
|
231
|
-
indent +
|
|
232
|
-
prop.body +
|
|
233
|
-
'\n' +
|
|
234
|
-
prop.fullText.substring(0, openBraceIndex).match(/^(\s*)/)[0] +
|
|
235
|
-
'}'
|
|
236
|
-
|
|
237
|
-
replacements.push({
|
|
238
|
-
start: prop.startPos,
|
|
239
|
-
end: prop.endPos,
|
|
240
|
-
text: modifiedPropText,
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
}
|
|
212
|
+
const funcName = functionNameMatch[1]
|
|
213
|
+
const funcNamePos = currentPos
|
|
214
|
+
|
|
215
|
+
// Find opening brace for this function
|
|
216
|
+
const afterSignaturePos = funcNamePos + functionNameMatch[0].length
|
|
217
|
+
const openingBracePos = computedObj.indexOf('{', afterSignaturePos)
|
|
218
|
+
if (openingBracePos === -1) break
|
|
219
|
+
|
|
220
|
+
// Find closing brace for individual computed function
|
|
221
|
+
const closingBracePos = findComputedFunctionEnd(computedObj, openingBracePos)
|
|
222
|
+
|
|
223
|
+
if (closingBracePos === -1) {
|
|
224
|
+
break
|
|
244
225
|
}
|
|
245
226
|
|
|
246
|
-
|
|
247
|
-
|
|
227
|
+
// Extract the entire function and its body
|
|
228
|
+
// This is better and more accurate than using regex to avoid nested braces
|
|
229
|
+
const functionStartPos = funcNamePos
|
|
230
|
+
const functionEndPos = closingBracePos
|
|
231
|
+
const fullFunction = computedObj.substring(functionStartPos, functionEndPos)
|
|
232
|
+
const functionBody = computedObj.substring(openingBracePos + 1, closingBracePos - 1).trim()
|
|
233
|
+
|
|
234
|
+
computedProps.push({
|
|
235
|
+
name: funcName,
|
|
236
|
+
fullText: fullFunction,
|
|
237
|
+
body: functionBody,
|
|
238
|
+
startPos: functionStartPos,
|
|
239
|
+
endPos: functionEndPos,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// Move past this function
|
|
243
|
+
currentPos = closingBracePos
|
|
244
|
+
}
|
|
248
245
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
// Store the original computed object for later replacement
|
|
247
|
+
const originalComputedObj = configObject.substring(computedBlockStart, computedBlockEnd)
|
|
248
|
+
const computedObjOffsetInOriginal = computedStart - computedBlockStart
|
|
249
|
+
const computedObjEndInOriginal = computedObjOffsetInOriginal + computedObj.length
|
|
250
|
+
|
|
251
|
+
let modifiedComputedObj = originalComputedObj
|
|
252
|
+
let hasChanges = false
|
|
253
|
+
|
|
254
|
+
const replacements = []
|
|
255
|
+
|
|
256
|
+
for (const prop of computedProps) {
|
|
257
|
+
const thisRefs = extractThisReferences(prop.body)
|
|
258
|
+
|
|
259
|
+
if (thisRefs.size > 0) {
|
|
260
|
+
// Skip if already modified
|
|
261
|
+
if (
|
|
262
|
+
prop.body.includes(commentText.trim()) ||
|
|
263
|
+
Array.from(thisRefs).some((ref) => {
|
|
264
|
+
if (prop.body.startsWith(ref)) return true
|
|
265
|
+
// stripped && guard: body starts with the first segment, e.g. 'this.media &&'
|
|
266
|
+
const firstSegEnd = ref.indexOf('.', 5)
|
|
267
|
+
return firstSegEnd !== -1 && prop.body.startsWith(ref.substring(0, firstSegEnd))
|
|
268
|
+
})
|
|
269
|
+
) {
|
|
270
|
+
continue
|
|
253
271
|
}
|
|
254
272
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
273
|
+
// reactivity code
|
|
274
|
+
const refCode = Array.from(thisRefs)
|
|
275
|
+
.map((ref) => `${toSafeRef(ref)};`)
|
|
276
|
+
.join(' ')
|
|
277
|
+
|
|
278
|
+
// replacement with the same whitespace
|
|
279
|
+
const openBraceIndex = prop.fullText.indexOf('{')
|
|
280
|
+
|
|
281
|
+
if (openBraceIndex !== -1) {
|
|
282
|
+
// Extract indentation from the original function body
|
|
283
|
+
const indentMatch = prop.body.match(/^(\s+)/)
|
|
284
|
+
const indent = indentMatch ? indentMatch[1] + ' ' : ' ' // Default to 4 spaces if no indent found
|
|
285
|
+
|
|
286
|
+
const modifiedPropText =
|
|
287
|
+
prop.fullText.substring(0, openBraceIndex + 1) +
|
|
288
|
+
'\n' +
|
|
289
|
+
indent +
|
|
290
|
+
commentText +
|
|
291
|
+
'\n' +
|
|
292
|
+
indent +
|
|
293
|
+
refCode +
|
|
294
|
+
'\n' +
|
|
295
|
+
indent +
|
|
296
|
+
prop.body +
|
|
297
|
+
'\n' +
|
|
298
|
+
prop.fullText.substring(0, openBraceIndex).match(/^(\s*)/)[0] +
|
|
299
|
+
'}'
|
|
300
|
+
|
|
301
|
+
replacements.push({
|
|
302
|
+
start: prop.startPos,
|
|
303
|
+
end: prop.endPos,
|
|
304
|
+
text: modifiedPropText,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
260
307
|
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (replacements.length > 0) {
|
|
311
|
+
let modifiedComputedInner = computedObj
|
|
261
312
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
replacement: modifiedComputedObj,
|
|
267
|
-
})
|
|
313
|
+
replacements.sort((a, b) => b.start - a.start)
|
|
314
|
+
for (const r of replacements) {
|
|
315
|
+
modifiedComputedInner =
|
|
316
|
+
modifiedComputedInner.slice(0, r.start) + r.text + modifiedComputedInner.slice(r.end)
|
|
268
317
|
}
|
|
318
|
+
|
|
319
|
+
modifiedComputedObj =
|
|
320
|
+
originalComputedObj.slice(0, computedObjOffsetInOriginal) +
|
|
321
|
+
modifiedComputedInner +
|
|
322
|
+
originalComputedObj.slice(computedObjEndInOriginal)
|
|
323
|
+
hasChanges = true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (hasChanges) {
|
|
327
|
+
modifications.push({
|
|
328
|
+
original: originalComputedObj,
|
|
329
|
+
replacement: modifiedComputedObj,
|
|
330
|
+
})
|
|
269
331
|
}
|
|
270
332
|
}
|
|
271
333
|
}
|
|
272
334
|
|
|
335
|
+
return modifications
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export default (code) => {
|
|
339
|
+
const componentOpenRegex = /Blits\.(Component|Application)\s*\(/g
|
|
340
|
+
const modifications = []
|
|
341
|
+
|
|
342
|
+
let match
|
|
343
|
+
while ((match = componentOpenRegex.exec(code)) !== null) {
|
|
344
|
+
const openParenPos = match.index + match[0].length - 1
|
|
345
|
+
const configStart = findConfigBraceStart(code, openParenPos)
|
|
346
|
+
if (configStart === -1) continue
|
|
347
|
+
|
|
348
|
+
const configEnd = findMatchingBrace(code, configStart)
|
|
349
|
+
if (configEnd === -1) continue
|
|
350
|
+
|
|
351
|
+
const configObject = code.substring(configStart, configEnd + 1)
|
|
352
|
+
modifications.push(...processComputedBlock(configObject))
|
|
353
|
+
}
|
|
354
|
+
|
|
273
355
|
// Apply all modifications at once, from last to first to preserve positions
|
|
274
356
|
if (modifications.length > 0) {
|
|
275
357
|
let modifiedCode = code
|