jq79 0.4.2 → 0.4.4

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/src/transform.ts CHANGED
@@ -22,7 +22,9 @@
22
22
 
23
23
  type SetupTransform = { vars: string[]; code: string }
24
24
 
25
- const DECLARATION_RE = /(?:let|var|const)\s+([A-Za-z_$][\w$]*)/y
25
+ // a declaration whose target is an identifier (`let x`), an object pattern
26
+ // (`let { a }`, space optional) or an array pattern (`let [x]`)
27
+ const DECLARATION_START_RE = /(?:let|var|const)(?:\s+(?=[A-Za-z_$])|\s*(?=[{[]))/y
26
28
  const REACTIVE_LABEL_RE = /\$:\s*/y
27
29
  const IMPORT_CALL_RE = /import(?=\s*\()/y
28
30
  const REACTIVE_ASSIGN_RE = /\$:\s*([A-Za-z_$][\w$]*)\s*=(?!=)/y
@@ -139,6 +141,28 @@ const skipRegex = (src: string, start: number): number => {
139
141
  // inserts the semicolon before them
140
142
  const CONTINUATION_RE = /^(\?\.|\?\?|&&|\|\||\*\*|[.,+\-*/%&|^<>=?:([])/
141
143
 
144
+ // the last meaningful character before `at`: walks back past whitespace and
145
+ // block comments, the way regexAllowed does. A line comment can't be skipped
146
+ // from behind (its start is only findable forwards), so a line ending in one
147
+ // reports the comment's text instead - callers treat that as "not the char I
148
+ // was looking for", which degrades to ending the statement, exactly as
149
+ // before this helper existed
150
+ const lastMeaningfulBefore = (src: string, at: number): string => {
151
+ let i = at - 1
152
+ while (i >= 0) {
153
+ const ch = src[i]
154
+ if (/\s/.test(ch)) { i--; continue }
155
+ if (ch === "/" && src[i - 1] === "*") {
156
+ const open = src.lastIndexOf("/*", i - 2)
157
+ if (open === -1) return ""
158
+ i = open - 1
159
+ continue
160
+ }
161
+ return ch
162
+ }
163
+ return ""
164
+ }
165
+
142
166
  // end of a statement starting at `start`: the first `;` or line break that
143
167
  // isn't inside a string/comment or unbalanced brackets. A line break only
144
168
  // ends the statement if the next line can't continue it, so leading-dot
@@ -147,6 +171,11 @@ const CONTINUATION_RE = /^(\?\.|\?\?|&&|\|\||\*\*|[.,+\-*/%&|^<>=?:([])/
147
171
  // $: total = items
148
172
  // .filter(item => item.active) <- still the same statement
149
173
  // .length
174
+ //
175
+ // ...and only if the current line *can* end it: a line whose last meaningful
176
+ // character is `,` or `=` left its expression incomplete (a multi-line
177
+ // declarator list writes exactly this), so the statement continues - the
178
+ // same ASI call as the leading-token check, made from the other side
150
179
  const findStatementEnd = (src: string, start: number): number => {
151
180
  let depth = 0
152
181
  let i = start
@@ -161,7 +190,10 @@ const findStatementEnd = (src: string, start: number): number => {
161
190
  else if (depth <= 0 && ch === ";") return i
162
191
  else if (depth <= 0 && ch === "\n") {
163
192
  const next = skipToToken(src, i + 1)
164
- if (next >= src.length || !CONTINUATION_RE.test(src.slice(next, next + 2))) return i
193
+ const continues =
194
+ next < src.length &&
195
+ (CONTINUATION_RE.test(src.slice(next, next + 2)) || [",", "="].includes(lastMeaningfulBefore(src, i)))
196
+ if (!continues) return i
165
197
  i = next
166
198
  continue
167
199
  }
@@ -170,6 +202,108 @@ const findStatementEnd = (src: string, start: number): number => {
170
202
  return src.length
171
203
  }
172
204
 
205
+ // ---------------------------------------------------------------------------
206
+ // top-level declarations. `let x = 1` loses its keyword and becomes a scope
207
+ // assignment (x pre-declared on the store). A destructuring declarator
208
+ // becomes an *assignment pattern*: inside `with`, `({ a, b } = obj)` writes
209
+ // every binding through the reactive proxy - which is what makes it reactive.
210
+ // The parens keep the `{` from opening a block, and a leading `;` keeps the
211
+ // `(` from gluing onto the previous line as a call. Multi-declarator
212
+ // statements (`let a = 1, b = 2`) register every binding, not just the first.
213
+ // These lean on the pattern helpers defined with the props signature below
214
+ // (splitTopLevel, indexOfTopLevel, defaultAssignIndex); the scanner only
215
+ // runs long after the module evaluates, so the order is cosmetic
216
+ // ---------------------------------------------------------------------------
217
+
218
+ type Declarator = { raw: string; codeEnd: number }
219
+
220
+ // splits a declarator list at top-level commas, keeping each segment's raw
221
+ // text (layout and comments included) and where its last meaningful token
222
+ // ends - the spot a closing paren must go, so a trailing comment can't
223
+ // swallow it
224
+ const splitDeclarators = (src: string): Declarator[] => {
225
+ const parts: Declarator[] = []
226
+ let depth = 0
227
+ let start = 0
228
+ let lastEnd = 0
229
+ const flush = (end: number) => {
230
+ parts.push({ raw: src.slice(start, end), codeEnd: Math.max(0, lastEnd - start) })
231
+ start = end + 1
232
+ lastEnd = start
233
+ }
234
+ let i = 0
235
+ while (i < src.length) {
236
+ const ch = src[i]
237
+ if (ch === "'" || ch === '"' || ch === "`") { i = skipString(src, i); lastEnd = i; continue }
238
+ if (ch === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
239
+ if (ch === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
240
+ if (ch === "/" && regexAllowed(src, i)) { i = skipRegex(src, i); lastEnd = i; continue }
241
+ if ("([{".includes(ch)) depth++
242
+ else if (")]}".includes(ch)) depth--
243
+ else if (ch === "," && depth <= 0) { flush(i); i++; continue }
244
+ if (!/\s/.test(ch)) lastEnd = i + 1
245
+ i++
246
+ }
247
+ flush(src.length)
248
+ return parts
249
+ }
250
+
251
+ // the *binding* names a destructuring pattern declares - unlike
252
+ // parsePropsPattern, which answers "which props" (the keys), this answers
253
+ // "which variables": `{ a: x }` binds x, `{ a: { b } }` binds b,
254
+ // `[x, ...rest]` binds x and rest. Defaults are stripped; what remains is a
255
+ // nested pattern (recurse) or the bound identifier
256
+ const patternBindings = (src: string): string[] => {
257
+ const pattern = src.trim()
258
+ if (!pattern.startsWith("{") && !pattern.startsWith("[")) {
259
+ return IDENTIFIER_RE.test(pattern) ? [pattern] : []
260
+ }
261
+ const names: string[] = []
262
+ for (let part of splitTopLevel(pattern.slice(1, patternCloseIndex(pattern)))) {
263
+ if (part.startsWith("...")) part = part.slice(3).trim()
264
+ const assign = defaultAssignIndex(part)
265
+ if (assign !== -1) part = part.slice(0, assign).trim()
266
+ if (pattern.startsWith("{")) {
267
+ const colon = indexOfTopLevel(part, ":")
268
+ if (colon !== -1) {
269
+ names.push(...patternBindings(part.slice(colon + 1)))
270
+ continue
271
+ }
272
+ }
273
+ names.push(...patternBindings(part))
274
+ }
275
+ return names
276
+ }
277
+
278
+ // one `let/var/const` declarator list, rewritten to scope assignments.
279
+ // Each segment's initializer is re-scanned so an `import()` inside it is
280
+ // rewritten like anywhere else - nothing else can match in there, since a
281
+ // nested top-level declaration inside a declarator is a SyntaxError in JS
282
+ const rewriteDeclarators = (src: string): SetupTransform => {
283
+ const vars: string[] = []
284
+ const rewritten = splitDeclarators(src).map(({ raw, codeEnd }) => {
285
+ const lead = raw.match(/^\s*/)![0]
286
+ if (codeEnd <= lead.length) return { text: raw, empty: true }
287
+ const body = raw.slice(lead.length, codeEnd)
288
+ const tail = raw.slice(codeEnd)
289
+ const assign = defaultAssignIndex(body)
290
+ const target = (assign === -1 ? body : body.slice(0, assign)).trim()
291
+ const isPattern = body[0] === "{" || body[0] === "["
292
+ if (isPattern) vars.push(...patternBindings(target))
293
+ else if (IDENTIFIER_RE.test(target)) vars.push(target)
294
+ const code = transformSetupScript(body).code
295
+ return { text: `${lead}${isPattern ? `(${code})` : code}${tail}`, empty: false }
296
+ })
297
+
298
+ // a trailing comment-only segment (`let a = 1, // note` cut at its line
299
+ // end) is re-attached without its comma, so the output stays a statement
300
+ const tail: string[] = []
301
+ while (rewritten.length && rewritten[rewritten.length - 1].empty) tail.unshift(rewritten.pop()!.text)
302
+ let code = rewritten.map(part => part.text).join(",") + tail.join("")
303
+ if (code.trimStart().startsWith("(")) code = `;${code}`
304
+ return { vars, code }
305
+ }
306
+
173
307
  export const transformSetupScript = (src: string): SetupTransform => {
174
308
  const vars: string[] = []
175
309
  let out = ""
@@ -217,12 +351,15 @@ export const transformSetupScript = (src: string): SetupTransform => {
217
351
  }
218
352
 
219
353
  if (depth === 0 && atStatementStart) {
220
- DECLARATION_RE.lastIndex = i
221
- const decl = DECLARATION_RE.exec(src)
354
+ DECLARATION_START_RE.lastIndex = i
355
+ const decl = DECLARATION_START_RE.exec(src)
222
356
  if (decl) {
223
- vars.push(decl[1])
224
- out += decl[1]
225
- i += decl[0].length
357
+ const start = i + decl[0].length
358
+ const end = findStatementEnd(src, start)
359
+ const { vars: names, code } = rewriteDeclarators(src.slice(start, end))
360
+ vars.push(...names)
361
+ out += code
362
+ i = end
226
363
  atStatementStart = false
227
364
  continue
228
365
  }
@@ -342,6 +479,24 @@ export type PropDecl = { name: string; default?: string }
342
479
 
343
480
  const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/
344
481
 
482
+ // index of the bracket that closes the one opening at src[0], skipping
483
+ // strings, comments and regex literals; src.length when unbalanced
484
+ const patternCloseIndex = (src: string): number => {
485
+ let depth = 0
486
+ let i = 0
487
+ while (i < src.length) {
488
+ const ch = src[i]
489
+ if (ch === "'" || ch === '"' || ch === "`") { i = skipString(src, i); continue }
490
+ if (ch === "/" && src[i + 1] === "/") { i = skipLineComment(src, i); continue }
491
+ if (ch === "/" && src[i + 1] === "*") { i = skipBlockComment(src, i); continue }
492
+ if (ch === "/" && regexAllowed(src, i)) { i = skipRegex(src, i); continue }
493
+ if ("([{".includes(ch)) depth++
494
+ else if (")]}".includes(ch) && --depth === 0) return i
495
+ i++
496
+ }
497
+ return src.length
498
+ }
499
+
345
500
  // index of the first `ch` at bracket depth 0, skipping strings and comments
346
501
  const indexOfTopLevel = (src: string, ch: string): number => {
347
502
  let depth = 0
@@ -407,18 +562,7 @@ export const parsePropsPattern = (pattern: string | undefined): PropDecl[] | nul
407
562
 
408
563
  // to the `}` that closes the pattern, so a parameter's own default value
409
564
  // (`({ label } = {})`) is left out of it
410
- let depth = 0
411
- let close = 0
412
- while (close < src.length) {
413
- const ch = src[close]
414
- if (ch === "'" || ch === '"' || ch === "`") { close = skipString(src, close); continue }
415
- if (ch === "/" && src[close + 1] === "/") { close = skipLineComment(src, close); continue }
416
- if (ch === "/" && src[close + 1] === "*") { close = skipBlockComment(src, close); continue }
417
- if (ch === "/" && regexAllowed(src, close)) { close = skipRegex(src, close); continue }
418
- if ("([{".includes(ch)) depth++
419
- else if (")]}".includes(ch) && --depth === 0) break
420
- close++
421
- }
565
+ const close = patternCloseIndex(src)
422
566
  if (close >= src.length) return null // unbalanced: not a pattern we can read
423
567
 
424
568
  const props: PropDecl[] = []