@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.
@@ -100,176 +100,258 @@ const skipNonCode = (text, pos) => {
100
100
  return p
101
101
  }
102
102
 
103
- export default (code) => {
104
- // Find component declarations
105
- const componentRegex =
106
- /Blits\.(Component|Application)\s*\(\s*(?:'[^']*'|"[^"]*")?\s*(?:,\s*)?({[\s\S]*?})\s*\)/g
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
- let match
111
- while ((match = componentRegex.exec(code)) !== null) {
112
- const configObject = match[2]
113
-
114
- // Find computed section with proper brace balancing
115
- const computedKeywordMatch = /computed\s*:\s*{/.exec(configObject)
116
-
117
- if (computedKeywordMatch) {
118
- // Find start position of the computed block
119
- const computedStart = computedKeywordMatch.index + computedKeywordMatch[0].length - 1 // Position of the opening brace
120
-
121
- // Find the matching closing brace for computed object in config
122
- const computedEnd = findComputedObjectEnd(configObject, computedStart)
123
-
124
- if (computedEnd !== -1) {
125
- const computedObj = configObject.substring(computedStart, computedEnd)
126
- const computedBlockStart = computedKeywordMatch.index
127
- const computedBlockEnd = computedEnd
128
-
129
- // Process each computed property
130
- const computedProps = []
131
- let currentPos = 1 // Start after the opening brace
132
-
133
- // Process computed properties content
134
- // intentionally not using regexes to extract functions and their content
135
- // using cursor-based parsing gives better accuracy
136
- while (currentPos < computedObj.length - 1) {
137
- // -1 to exclude the closing brace
138
- // Skip whitespace/comments so we don't match commented-out computed functions
139
- currentPos = skipNonCode(computedObj, currentPos)
140
- if (currentPos >= computedObj.length - 1) break
141
-
142
- // Find the next function name (computed properties are `name() { ... }`)
143
- const functionNameMatch = /^(\w+)\s*\(\)\s*/.exec(computedObj.substring(currentPos))
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
- // Store the original computed object for later replacement
183
- const originalComputedObj = configObject.substring(computedBlockStart, computedBlockEnd)
184
- const computedObjOffsetInOriginal = computedStart - computedBlockStart
185
- const computedObjEndInOriginal = computedObjOffsetInOriginal + computedObj.length
186
-
187
- let modifiedComputedObj = originalComputedObj
188
- let hasChanges = false
189
-
190
- const replacements = []
191
-
192
- for (const prop of computedProps) {
193
- const thisRefs = extractThisReferences(prop.body)
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
- if (replacements.length > 0) {
247
- let modifiedComputedInner = computedObj
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
- replacements.sort((a, b) => b.start - a.start)
250
- for (const r of replacements) {
251
- modifiedComputedInner =
252
- modifiedComputedInner.slice(0, r.start) + r.text + modifiedComputedInner.slice(r.end)
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
- modifiedComputedObj =
256
- originalComputedObj.slice(0, computedObjOffsetInOriginal) +
257
- modifiedComputedInner +
258
- originalComputedObj.slice(computedObjEndInOriginal)
259
- hasChanges = true
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
- if (hasChanges) {
263
- // Store the modification of the entire computed object
264
- modifications.push({
265
- original: originalComputedObj,
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