parseman 0.60.1 → 0.60.2

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.
@@ -5728,7 +5728,7 @@ function projectChild(children2, project, type) {
5728
5728
  }
5729
5729
 
5730
5730
  // src/version.ts
5731
- var PARSEMAN_VERSION = "0.60.1";
5731
+ var PARSEMAN_VERSION = "0.60.2";
5732
5732
 
5733
5733
  // src/table/assemble.ts
5734
5734
  import { PlainBuf as PlainBuf2, PlainField, PlainLeaf as PlainLeaf8, PlainNode as PlainNode2, PlainSpan as PlainSpan9 } from "../cst/plain.js";
package/dist/version.d.ts CHANGED
@@ -13,5 +13,5 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export declare const PARSEMAN_VERSION = "0.60.1";
16
+ export declare const PARSEMAN_VERSION = "0.60.2";
17
17
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parseman",
3
- "version": "0.60.1",
3
+ "version": "0.60.2",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || >=22.13.0"
@@ -83,29 +83,60 @@ const TABLE_RUNTIME_SPECIFIER = 'parseman/table'
83
83
  * from the artifact's own location.
84
84
  */
85
85
  export type CarriedSidecarItem = IRPiece | { from: string; name: string }
86
+ /**
87
+ * A recorded item: a reference also carries the pieces it stands for, expanded, so a
88
+ * build that BUNDLES the referenced package rather than keeping it external writes
89
+ * those pieces instead of a reference to a package its artifact no longer imports
90
+ * (`emitCarriedSidecars`).
91
+ */
92
+ export type CarriedSinkItem = IRPiece | { from: string; name: string; expanded?: readonly CarriedSidecarItem[] }
86
93
  /** One module's carried lists, by export name. */
87
- export type CarriedSink = Map<string, CarriedSidecarItem[]>
94
+ export type CarriedSink = Map<string, CarriedSinkItem[]>
88
95
  export const CARRIED_SIDECAR_SUFFIX = '.parseman.json'
89
- type CarriedSidecar = { parseman: string; exports: Record<string, CarriedSidecarItem[]> }
96
+ /**
97
+ * `artifact` is `artifactDigest` of the chunk the sidecar was written beside. A
98
+ * reader that finds a different artifact there is reading a sidecar some other
99
+ * build left behind, and fails. A sidecar without one predates the check.
100
+ */
101
+ type CarriedSidecar = { parseman: string; artifact?: string; exports: Record<string, CarriedSidecarItem[]> }
90
102
 
91
- const _sidecarCache = new Map<string, { mtimeMs: number; sidecar: CarriedSidecar | null }>()
92
- /** The carried-pieces sidecar written beside a compiled artifact, or null. */
93
- function readCarriedSidecar(artifact: string): CarriedSidecar | null {
103
+ /**
104
+ * The digest a sidecar records of its artifact: SHA-256 of the chunk's code, less a
105
+ * trailing `sourceMappingURL` comment, which a bundler appends after the hook that
106
+ * writes the sidecar has seen the code.
107
+ */
108
+ export function artifactDigest(code: string): string {
109
+ const body = code.replace(/\n?\/\/# sourceMappingURL=[^\n]*\n?$/, '')
110
+ return createHash('sha256').update(body).digest('hex').slice(0, 32)
111
+ }
112
+
113
+ type SidecarRead = { sidecar: CarriedSidecar; stale: boolean } | null
114
+ const _sidecarCache = new Map<string, { mtimeMs: number; artifactMtimeMs: number; read: SidecarRead }>()
115
+ /**
116
+ * The carried-pieces sidecar written beside a compiled artifact, or null; `stale`
117
+ * when it records a digest the artifact beside it does not have.
118
+ */
119
+ function readCarriedSidecar(artifact: string): SidecarRead {
94
120
  const file = artifact + CARRIED_SIDECAR_SUFFIX
95
121
  let mtimeMs: number
96
- try { mtimeMs = fs.statSync(file).mtimeMs } catch { return null }
122
+ let artifactMtimeMs: number
123
+ try { mtimeMs = fs.statSync(file).mtimeMs; artifactMtimeMs = fs.statSync(artifact).mtimeMs } catch { return null }
97
124
  const hit = _sidecarCache.get(file)
98
- if (hit && hit.mtimeMs === mtimeMs) return hit.sidecar
99
- let sidecar: CarriedSidecar | null = null
125
+ if (hit && hit.mtimeMs === mtimeMs && hit.artifactMtimeMs === artifactMtimeMs) return hit.read
126
+ let read: SidecarRead = null
100
127
  try {
101
128
  // A file without an `exports` object is not a sidecar this plugin wrote: treat
102
129
  // it as absent, so the inline literal is looked for and a missing one reports.
103
130
  const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
104
131
  const exports = typeof parsed === 'object' && parsed !== null ? (parsed as { exports?: unknown }).exports : undefined
105
- if (typeof exports === 'object' && exports !== null) sidecar = parsed as CarriedSidecar
132
+ if (typeof exports === 'object' && exports !== null) {
133
+ const sidecar = parsed as CarriedSidecar
134
+ const stale = typeof sidecar.artifact === 'string' && sidecar.artifact !== artifactDigest(fs.readFileSync(artifact, 'utf8'))
135
+ read = { sidecar, stale }
136
+ }
106
137
  } catch { /* unreadable: no sidecar */ }
107
- _sidecarCache.set(file, { mtimeMs, sidecar })
108
- return sidecar
138
+ _sidecarCache.set(file, { mtimeMs, artifactMtimeMs, read })
139
+ return read
109
140
  }
110
141
 
111
142
  export type ParsecraftPluginOptions = {
@@ -360,7 +391,9 @@ function parseModuleCached(filePath: string): ParsedModule | null {
360
391
  }
361
392
 
362
393
  /** The rollup-family hook that writes each chunk's carried pieces beside it. */
363
- type SidecarBundle = Record<string, { type: string; fileName: string; exports?: string[]; moduleIds?: string[]; facadeModuleId?: string | null }>
394
+ type SidecarBundle = Record<string, { type: string; fileName: string; code?: string; exports?: string[]; moduleIds?: string[]; facadeModuleId?: string | null }>
395
+ /** A module's recorded list for one export, and the module that recorded it. */
396
+ type Recorded = { readonly items: readonly CarriedSinkItem[]; readonly recorder: string }
364
397
  type SidecarEmitter = {
365
398
  emitFile: (file: { type: 'asset'; fileName: string; source: string }) => unknown
366
399
  getModuleInfo: (id: string) => { code: string | null } | null
@@ -368,77 +401,131 @@ type SidecarEmitter = {
368
401
  }
369
402
 
370
403
  /**
371
- * Where a module's export `exported` comes from when the module re-exports it,
372
- * possibly renamed: `export { base as renamed } from './grammar'`, or an imported
373
- * binding exported under another name. Null when the module declares it itself.
404
+ * Where a module's export `exported` comes from: a re-export, possibly renamed
405
+ * (`export { base as renamed } from './grammar'`, or an imported binding exported
406
+ * under another name); the module's own declaration (`'local'`); or, when the
407
+ * module neither declares nor names it, its `export * from` sources, in source
408
+ * order (none: a form this does not read, such as `export default`).
374
409
  */
375
- function reexportSource(body: AnyNode[], exported: string): { source: string; imported: string } | null {
410
+ type ExportOrigin = { readonly from: string; readonly imported: string } | { readonly stars: readonly string[] } | 'local'
411
+
412
+ function exportOrigin(body: AnyNode[], exported: string): ExportOrigin {
413
+ const stars: string[] = []
376
414
  for (const st of body) {
415
+ if (st.type === 'ExportAllDeclaration') {
416
+ const from = (st.source as { value?: string } | null | undefined)?.value
417
+ if (st.exported == null && from !== undefined) stars.push(from)
418
+ continue
419
+ }
377
420
  if (st.type !== 'ExportNamedDeclaration') continue
421
+ const decl = st.declaration as AnyNode | null | undefined
422
+ if (decl != null) {
423
+ const ids = decl.type === 'VariableDeclaration'
424
+ ? ((decl.declarations as AnyNode[] | undefined) ?? []).map(d => (d.id as { name?: string } | undefined)?.name)
425
+ : [(decl.id as { name?: string } | null | undefined)?.name]
426
+ if (ids.includes(exported)) return 'local'
427
+ }
378
428
  for (const sp of (st.specifiers as AnyNode[] | undefined) ?? []) {
379
429
  const exp = sp.exported as { name?: string; value?: string } | undefined
380
430
  if ((exp?.name ?? exp?.value) !== exported) continue
381
431
  const local = sp.local as { name?: string; value?: string } | undefined
382
432
  const name = local?.name ?? local?.value
383
- if (name === undefined) return null
433
+ if (name === undefined) return 'local'
384
434
  const from = (st.source as { value?: string } | null | undefined)?.value
385
- return from !== undefined ? { source: from, imported: name } : extractImportBindings(body).get(name) ?? null
435
+ if (from !== undefined) return { from, imported: name }
436
+ const imported = extractImportBindings(body).get(name)
437
+ return imported === undefined ? 'local' : { from: imported.source, imported: imported.imported }
386
438
  }
387
439
  }
388
- return null
440
+ return { stars }
389
441
  }
390
442
 
391
443
  /**
392
444
  * The carried list behind an entry chunk's export, followed from the entry module
393
- * through its re-exports to the module that recorded it, under the name that module
394
- * exports it as. A chunk's export names are its entry module's, so an entry that
395
- * renames a grammar (`export { base as renamed } from './grammar'`) still finds the
396
- * pieces recorded as `base`.
445
+ * through its re-exports (named or `export *`) to the module that declares it,
446
+ * under the name that module exports it as. A chunk's export names are its entry
447
+ * module's, so an entry that renames a grammar (`export { base as renamed } from
448
+ * './grammar'`) still finds the pieces recorded as `base`.
449
+ *
450
+ * `null` when the export is traced to a module that recorded nothing under that
451
+ * name — a precompiled dependency, or a value that is not a grammar: it has no
452
+ * carried pieces, and no other module's may stand in for them. `undefined` when
453
+ * the trace cannot be completed (no module code, an external, a parse error).
397
454
  */
398
- async function followCarried(this: SidecarEmitter, sinks: Map<string, CarriedSink>, moduleId: string, name: string): Promise<CarriedSidecarItem[] | undefined> {
399
- for (let hop = 0; hop < 64; hop++) {
400
- const own = sinks.get(moduleId)?.get(name)
401
- if (own !== undefined) return own
402
- const code = this.getModuleInfo(moduleId)?.code
403
- if (!code) return undefined
404
- const parsed = parseSync(moduleId, code)
405
- if (parsed.errors.length > 0) return undefined
406
- const next = reexportSource(parsed.program.body as unknown as AnyNode[], name)
407
- if (next === null) return undefined
408
- const resolved = await this.resolve(next.source, moduleId)
455
+ async function followCarried(
456
+ this: SidecarEmitter, sinks: Map<string, CarriedSink>, moduleId: string, name: string, depth = 0,
457
+ ): Promise<Recorded | null | undefined> {
458
+ if (depth > 64) return undefined
459
+ const own = sinks.get(moduleId)?.get(name)
460
+ if (own !== undefined) return { items: own, recorder: moduleId }
461
+ const code = this.getModuleInfo(moduleId)?.code
462
+ if (!code) return undefined
463
+ const parsed = parseSync(moduleId, code)
464
+ if (parsed.errors.length > 0) return undefined
465
+ const origin = exportOrigin(parsed.program.body as unknown as AnyNode[], name)
466
+ if (origin === 'local') return null
467
+ const follow = async (source: string, imported: string): Promise<Recorded | null | undefined> => {
468
+ const resolved = await this.resolve(source, moduleId)
409
469
  if (resolved === null || resolved.external) return undefined
410
- moduleId = resolved.id
411
- name = next.imported
412
- }
413
- return undefined
470
+ return followCarried.call(this, sinks, resolved.id, imported, depth + 1)
471
+ }
472
+ if ('from' in origin) return follow(origin.from, origin.imported)
473
+ // `export *` re-exports a name only from the one star source that has it; two
474
+ // that reach different declarations make it ambiguous, and ESM exports neither.
475
+ // Two paths to the same declaration are one binding.
476
+ let found: Recorded | undefined
477
+ let unknown = false
478
+ for (const source of origin.stars) {
479
+ const got = await follow(source, name)
480
+ if (got === undefined) unknown = true
481
+ else if (got !== null) {
482
+ if (found !== undefined && found.items !== got.items) return null
483
+ found = got
484
+ }
485
+ }
486
+ return found ?? (unknown || origin.stars.length === 0 ? undefined : null)
414
487
  }
415
488
 
416
489
  /**
417
490
  * Write `<chunk>.parseman.json` for every chunk that exports a grammar with carried
418
491
  * pieces. An entry chunk's export is followed from its entry module to the module
419
- * that recorded it (`followCarried`). Any other export is matched by name to the
420
- * module that recorded it, preferring a module of the chunk itself; a name two
421
- * modules of the bundle both record, and neither of them in the chunk, is ambiguous
422
- * and is not written — a downstream compose of it then fails its build naming the
423
- * artifact.
492
+ * that declares it (`followCarried`). An export that cannot be followed is matched
493
+ * by name to the one module of the chunk itself that recorded it. Nothing else is
494
+ * written: a module outside the chunk recording the same name is not the export's
495
+ * source, and a downstream compose of an export with no pieces fails its build
496
+ * naming the artifact.
424
497
  */
425
498
  async function emitCarriedSidecars(this: SidecarEmitter, sinks: Map<string, CarriedSink>, bundle: SidecarBundle): Promise<void> {
426
- const recorders = new Map<string, string[]>()
427
- for (const [moduleId, sink] of sinks) for (const name of sink.keys()) (recorders.get(name) ?? recorders.set(name, []).get(name)!).push(moduleId)
499
+ // A reference names an export of another package, resolved from the artifact's
500
+ // location. That holds only where this build keeps the package EXTERNAL; a
501
+ // bundled one is in the chunk and may be missing, or a different version,
502
+ // wherever the artifact is composed. So a bundled reference is written expanded.
503
+ const write = async (rec: Recorded): Promise<CarriedSidecarItem[]> => {
504
+ const out: CarriedSidecarItem[] = []
505
+ for (const item of rec.items) {
506
+ if ('ir' in item) { out.push(item); continue }
507
+ const resolved = item.expanded === undefined ? null : await this.resolve(item.from, rec.recorder)
508
+ if (resolved !== null && !resolved.external) out.push(...item.expanded!)
509
+ else out.push({ from: item.from, name: item.name })
510
+ }
511
+ return out
512
+ }
428
513
  for (const chunk of Object.values(bundle)) {
429
514
  if (chunk.type !== 'chunk') continue
430
515
  const exports: Record<string, CarriedSidecarItem[]> = {}
431
516
  for (const name of chunk.exports ?? []) {
432
517
  const followed = chunk.facadeModuleId ? await followCarried.call(this, sinks, chunk.facadeModuleId, name) : undefined
433
- if (followed !== undefined) { exports[name] = followed; continue }
434
- const ids = recorders.get(name)
435
- if (ids === undefined) continue
436
- const own = ids.filter(m => chunk.moduleIds?.includes(m))
437
- const moduleId = own.length === 1 ? own[0] : own.length === 0 && ids.length === 1 ? ids[0] : undefined
438
- if (moduleId !== undefined) exports[name] = sinks.get(moduleId)!.get(name)!
518
+ if (followed === null) continue
519
+ if (followed !== undefined) { exports[name] = await write(followed); continue }
520
+ const own = (chunk.moduleIds ?? []).filter(m => sinks.get(m)?.has(name) === true)
521
+ if (own.length === 1) exports[name] = await write({ items: sinks.get(own[0]!)!.get(name)!, recorder: own[0]! })
439
522
  }
440
523
  if (Object.keys(exports).length === 0) continue
441
- const sidecar: CarriedSidecar = { parseman: PARSEMAN_VERSION, exports }
524
+ const sidecar: CarriedSidecar = {
525
+ parseman: PARSEMAN_VERSION,
526
+ ...(chunk.code === undefined ? {} : { artifact: artifactDigest(chunk.code) }),
527
+ exports,
528
+ }
442
529
  this.emitFile({ type: 'asset', fileName: chunk.fileName + CARRIED_SIDECAR_SUFFIX, source: JSON.stringify(sidecar) })
443
530
  }
444
531
  }
@@ -449,8 +536,14 @@ export default createUnplugin((opts: ParsecraftPluginOptions = {}, meta) => {
449
536
  // Module id → its carried lists, under a bundler that writes them beside the chunk.
450
537
  // Any other bundler gets them inline, as before.
451
538
  const sinks = SIDECAR_FRAMEWORKS.has(meta.framework) ? new Map<string, CarriedSink>() : undefined
539
+ // `order: 'post'`: the sidecar records a digest of each chunk's code, so it is
540
+ // taken after every other plugin's `generateBundle` has rewritten that code (Vite
541
+ // rewrites a dynamic `import()` through its preload helper there).
452
542
  const generateBundle = sinks === undefined ? undefined : {
453
- generateBundle(this: SidecarEmitter, _options: unknown, bundle: SidecarBundle) { return emitCarriedSidecars.call(this, sinks, bundle) },
543
+ generateBundle: {
544
+ order: 'post' as const,
545
+ handler(this: SidecarEmitter, _options: unknown, bundle: SidecarBundle) { return emitCarriedSidecars.call(this, sinks, bundle) },
546
+ },
454
547
  }
455
548
  return {
456
549
  name: 'parseman',
@@ -465,6 +558,9 @@ export default createUnplugin((opts: ParsecraftPluginOptions = {}, meta) => {
465
558
  },
466
559
 
467
560
  transform(this: { warn?: (msg: string) => void; addWatchFile?: (file: string) => void }, code: string, id: string) {
561
+ // What this module recorded on an earlier build of a watch session is stale
562
+ // from here: it records again below, or it no longer lowers anything.
563
+ sinks?.delete(id)
468
564
  if (!code.includes('parseman')) return null
469
565
  if (!code.includes('macro')) return null
470
566
  // A corpus edit changes which sites a profiled build generates, so a watch
@@ -473,10 +569,7 @@ export default createUnplugin((opts: ParsecraftPluginOptions = {}, meta) => {
473
569
  const moduleAliases = new Set([PARSEMAN_MODULE, ...(opts.moduleAliases ?? [])])
474
570
  const sink: CarriedSink | undefined = sinks && new Map()
475
571
  const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true, opts.assemblyProfile, opts.assemblyBudget, sink)
476
- if (sinks && sink) {
477
- if (sink.size > 0) sinks.set(id, sink)
478
- else sinks.delete(id)
479
- }
572
+ if (sinks && sink && sink.size > 0) sinks.set(id, sink)
480
573
  if (result?.warnings.length) {
481
574
  for (const w of result.warnings) {
482
575
  if (typeof this?.warn === 'function') this.warn(`[parseman] ${w}`)
@@ -1797,7 +1890,13 @@ function transformMacroImpl(
1797
1890
  const fail = (why: string): null => { unresolved.why ??= `${exportName} in ${file}: ${why}`; return null }
1798
1891
  // A build-only sidecar wins over an inline literal: an artifact that has one
1799
1892
  // carries no literal (see `CarriedSidecarItem`).
1800
- const sidecarItems = module.source || local ? undefined : readCarriedSidecar(file)?.exports[exportName]
1893
+ const sidecarRead = module.source || local ? null : readCarriedSidecar(file)
1894
+ if (sidecarRead?.stale === true) {
1895
+ return fail(`${path.basename(file)}${CARRIED_SIDECAR_SUFFIX} was written for a different build of ${path.basename(file)} `
1896
+ + '(the artifact digest it records does not match). Rebuild the package so the two are written together')
1897
+ }
1898
+ const sidecarExports = sidecarRead?.sidecar.exports
1899
+ const sidecarItems = sidecarExports !== undefined && Object.hasOwn(sidecarExports, exportName) ? sidecarExports[exportName] : undefined
1801
1900
  if (sidecarItems !== undefined) {
1802
1901
  const list: RawItem[] = []
1803
1902
  for (const item of sidecarItems) {
@@ -1821,10 +1920,32 @@ function transformMacroImpl(
1821
1920
  if (!literalRange) {
1822
1921
  // A compiled grammar with neither a literal nor a sidecar entry was built to be
1823
1922
  // composed or it was not, and a runtime compose() cannot link it either way.
1824
- return initNode !== null && !module.source && mod.src.slice(initNode.start, initNode.end).includes('tableRules(')
1825
- ? fail(`is a compiled grammar with no carried pieces — no inline literal, and no entry in ${path.basename(file)}${CARRIED_SIDECAR_SUFFIX}. `
1923
+ if (initNode !== null && !module.source && mod.src.slice(initNode.start, initNode.end).includes('tableRules(')) {
1924
+ return fail(`is a compiled grammar with no carried pieces — no inline literal, and no entry in ${path.basename(file)}${CARRIED_SIDECAR_SUFFIX}. `
1826
1925
  + 'Rebuild it with this parseman, and keep that file beside the artifact')
1827
- : null
1926
+ }
1927
+ // A COMPILED ARTIFACT RE-EXPORTING ITS GRAMMAR — a bundler's facade chunk,
1928
+ // `import { t as grammar } from './shared-HASH.js'; export { grammar }`, or an
1929
+ // `export *` entry. Follow the export to the module that declares it. A
1930
+ // compiled artifact whose export cannot be followed to carried pieces fails
1931
+ // the build: falling back to a runtime compose() of it would throw at import.
1932
+ if (module.source || local) return null
1933
+ const origin = exportOrigin(mod.body as AnyNode[], exportName)
1934
+ const targets = origin === 'local' ? [] : 'from' in origin
1935
+ ? [{ source: origin.from, imported: origin.imported }]
1936
+ : origin.stars.map(source => ({ source, imported: exportName }))
1937
+ for (const target of targets) {
1938
+ const sub = resolveGrammarModule(file, target.source)
1939
+ if (sub === null || seen.has(sub.file)) continue
1940
+ const pieces = resolveModulePieces(sub, target.imported, new Set(seen).add(sub.file), unresolved)
1941
+ if (pieces !== null) return pieces
1942
+ }
1943
+ // Neither this module nor anything it re-exports from is a compiled grammar:
1944
+ // not a build-compiled argument, so the caller decides.
1945
+ if (unresolved.why === undefined && !mod.src.includes(TABLE_RUNTIME_SPECIFIER)) return null
1946
+ return fail(`is exported by a compiled grammar artifact, but no carried pieces could be found for it — no inline literal, `
1947
+ + `no entry in ${path.basename(file)}${CARRIED_SIDECAR_SUFFIX}, and no re-export of it that has either. `
1948
+ + 'Rebuild it with this parseman, and keep each chunk\'s .parseman.json beside it')
1828
1949
  }
1829
1950
  const literal = mod.src.slice(literalRange.start, literalRange.end)
1830
1951
 
@@ -1909,12 +2030,17 @@ function transformMacroImpl(
1909
2030
  */
1910
2031
  const sinkCarried = (name: string, list: CarriedItem[]): boolean => {
1911
2032
  if (carriedSink === undefined) return false
1912
- const out: CarriedSidecarItem[] = []
2033
+ const out: CarriedSinkItem[] = []
1913
2034
  for (const it of list) {
1914
2035
  if (!isSpread(it)) { out.push(it); continue }
1915
2036
  const b = importBindings.get(it.__spreadLocal)
1916
2037
  if (b === undefined) return false
1917
- if (!b.source.startsWith('.') && !path.isAbsolute(b.source)) { out.push({ from: b.source, name: b.imported }); continue }
2038
+ if (!b.source.startsWith('.') && !path.isAbsolute(b.source)) {
2039
+ // Expanded too, for a build that bundles the package (`emitCarriedSidecars`).
2040
+ const expanded = importedPieces(it.__spreadLocal)
2041
+ out.push({ from: b.source, name: b.imported, ...(expanded === null ? {} : { expanded: expanded as CarriedSidecarItem[] }) })
2042
+ continue
2043
+ }
1918
2044
  const pieces = importedPieces(it.__spreadLocal)
1919
2045
  if (pieces === null) return false
1920
2046
  out.push(...pieces)
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export const PARSEMAN_VERSION = '0.60.1'
16
+ export const PARSEMAN_VERSION = '0.60.2'