parseman 0.61.3 → 0.61.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/dist/analysis/diagnostics.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/index.d.ts +22 -3
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +49 -23
- package/dist/plugin/index.js.map +1 -1
- package/dist/table/index.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/plugin/index.ts +76 -23
- package/src/version.ts +1 -1
package/dist/table/index.js
CHANGED
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.61.
|
|
16
|
+
export declare const PARSEMAN_VERSION = "0.61.4";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
package/src/plugin/index.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import * as fs from 'node:fs'
|
|
22
22
|
import * as path from 'node:path'
|
|
23
23
|
import { createUnplugin } from 'unplugin'
|
|
24
|
+
import type { UnpluginBuildContext } from 'unplugin'
|
|
24
25
|
import { parseSync } from 'oxc-parser'
|
|
25
26
|
import { ResolverFactory } from 'oxc-resolver'
|
|
26
27
|
import MagicString from 'magic-string'
|
|
@@ -183,13 +184,29 @@ export type ParsecraftPluginOptions = {
|
|
|
183
184
|
|
|
184
185
|
/** A corpus a build parses to rank selective-assembly sites — see `assemblyProfile`. */
|
|
185
186
|
export type AssemblyProfile = {
|
|
186
|
-
/** The rule each corpus file is parsed from, e.g. `'Stylesheet'`. A grammar
|
|
187
|
-
*
|
|
187
|
+
/** The rule each corpus file is parsed from, e.g. `'Stylesheet'`. A grammar without
|
|
188
|
+
* this rule keeps the static ranking. A build fails when grammars were profiled and
|
|
189
|
+
* none has the rule. A direct `transformMacro` caller gets `assemblyProfile:
|
|
190
|
+
* 'no-entry'` on the result instead, and has to read it. */
|
|
188
191
|
readonly entry: string
|
|
189
192
|
/** Paths of representative inputs. At least one must have content, and each that
|
|
190
193
|
* does has to parse in full without recovery, or the build fails. A file with only
|
|
191
|
-
* whitespace counts as empty.
|
|
194
|
+
* whitespace counts as empty. A callback the corpus doesn't reach is never run, so
|
|
195
|
+
* nothing checks it. */
|
|
192
196
|
readonly files: readonly string[]
|
|
197
|
+
/**
|
|
198
|
+
* The values of names a grammar's reducers and gates import, for the corpus run.
|
|
199
|
+
* Their source runs at build time with no module around it, so a grammar whose
|
|
200
|
+
* callbacks read imported helpers passes those helpers here. See
|
|
201
|
+
* `docs/guide/macro-mode.md` for what this does and doesn't cover.
|
|
202
|
+
*
|
|
203
|
+
* It's one flat namespace, so two modules can't bind one name to different helpers.
|
|
204
|
+
* A name that's already a global fails the build. A helper named like a global and
|
|
205
|
+
* left out of `scope` silently uses the global. A direct `node()` builder doesn't
|
|
206
|
+
* run in the corpus run: a plain node stands in for it, so a helper only a builder
|
|
207
|
+
* reads is neither needed here nor checked.
|
|
208
|
+
*/
|
|
209
|
+
readonly scope?: Readonly<Record<string, unknown>>
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
/** Corpus file contents, re-read when a file's mtime moves, so a watch build that
|
|
@@ -558,6 +575,9 @@ export default createUnplugin((opts: ParsecraftPluginOptions = {}, meta) => {
|
|
|
558
575
|
// Module id → its carried lists, under a bundler that writes them beside the chunk.
|
|
559
576
|
// Any other bundler gets them inline, as before.
|
|
560
577
|
const sinks = SIDECAR_FRAMEWORKS.has(meta.framework) ? new Map<string, CarriedSink>() : undefined
|
|
578
|
+
// Module id → whether its grammars ran the corpus. A module without the entry rule is
|
|
579
|
+
// fine on its own, because one plugin sees many modules, so only the build can tell.
|
|
580
|
+
const profiled = new Map<string, NonNullable<TransformMacroResult['assemblyProfile']>>()
|
|
561
581
|
// `order: 'post'`: the sidecar records a digest of each chunk's code, so it is
|
|
562
582
|
// taken after every other plugin's `generateBundle` has rewritten that code (Vite
|
|
563
583
|
// rewrites a dynamic `import()` through its preload helper there).
|
|
@@ -579,10 +599,19 @@ export default createUnplugin((opts: ParsecraftPluginOptions = {}, meta) => {
|
|
|
579
599
|
return /\.[jt]sx?$/.test(id) && !id.includes('node_modules')
|
|
580
600
|
},
|
|
581
601
|
|
|
602
|
+
buildEnd(this: UnpluginBuildContext & { getModuleIds?: () => Iterable<string> }, error?: unknown) {
|
|
603
|
+
if (error) return
|
|
604
|
+
// esbuild, webpack and rspack can't list a build's modules, so there every module seen counts.
|
|
605
|
+
const ids = [...this.getModuleIds?.() ?? profiled.keys()].filter(id => profiled.has(id))
|
|
606
|
+
if (ids.length === 0 || ids.some(id => profiled.get(id) === 'ran')) return
|
|
607
|
+
throw new Error(`assemblyProfile: no grammar has rule "${opts.assemblyProfile?.entry}", so the corpus never ran (${ids.join(', ')})`)
|
|
608
|
+
},
|
|
609
|
+
|
|
582
610
|
transform(this: { warn?: (msg: string) => void; addWatchFile?: (file: string) => void }, code: string, id: string) {
|
|
583
611
|
// What this module recorded on an earlier build of a watch session is stale
|
|
584
612
|
// from here: it records again below, or it no longer lowers anything.
|
|
585
613
|
sinks?.delete(id)
|
|
614
|
+
profiled.delete(id)
|
|
586
615
|
if (!code.includes('parseman')) return null
|
|
587
616
|
if (!code.includes('macro')) return null
|
|
588
617
|
// A corpus edit changes which sites a profiled build generates, so a watch
|
|
@@ -592,6 +621,7 @@ export default createUnplugin((opts: ParsecraftPluginOptions = {}, meta) => {
|
|
|
592
621
|
const sink: CarriedSink | undefined = sinks && new Map()
|
|
593
622
|
const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true, opts.assemblyProfile, opts.assemblyBudget, sink)
|
|
594
623
|
if (sinks && sink && sink.size > 0) sinks.set(id, sink)
|
|
624
|
+
if (result?.assemblyProfile !== undefined) profiled.set(id, result.assemblyProfile)
|
|
595
625
|
if (result?.warnings.length) {
|
|
596
626
|
for (const w of result.warnings) {
|
|
597
627
|
if (typeof this?.warn === 'function') this.warn(`[parseman] ${w}`)
|
|
@@ -786,6 +816,9 @@ export type TransformMacroResult = {
|
|
|
786
816
|
map: ReturnType<MagicString['generateMap']>
|
|
787
817
|
/** Diagnostics for macro-referencing shapes that fell back to the interpreter. */
|
|
788
818
|
warnings: string[]
|
|
819
|
+
/** With an `assemblyProfile`: `'ran'` when a grammar here parsed the corpus,
|
|
820
|
+
* `'no-entry'` when the grammars that could have don't have its `entry` rule. */
|
|
821
|
+
assemblyProfile?: 'ran' | 'no-entry'
|
|
789
822
|
/** The sites the module's `assemblyBudget` bought, by program offset, in rank order. */
|
|
790
823
|
assemblySites: number[]
|
|
791
824
|
}
|
|
@@ -2403,10 +2436,15 @@ function transformMacroImpl(
|
|
|
2403
2436
|
/** Measured site calls over the optional corpus, or `undefined` for the static
|
|
2404
2437
|
* ranking. A corpus that cannot run fails the build: a static ranking standing in
|
|
2405
2438
|
* for the measured one configured is a slower parser that nothing reports. */
|
|
2439
|
+
let profileOutcome: TransformMacroResult['assemblyProfile']
|
|
2406
2440
|
const profileOf = (compiled: CompiledRuleMapTable): Int32Array | undefined => {
|
|
2407
2441
|
// Only the default AST/no-lines table carries an assembly, so only it is profiled.
|
|
2408
|
-
if (assemblyProfile === undefined || compiled.hostMode !== 'ast' || compiled.prog.lines === 1
|
|
2409
|
-
|
|
2442
|
+
if (assemblyProfile === undefined || compiled.hostMode !== 'ast' || compiled.prog.lines === 1) return undefined
|
|
2443
|
+
if (!(assemblyProfile.entry in compiled.prog.rules)) {
|
|
2444
|
+
profileOutcome ??= 'no-entry'
|
|
2445
|
+
return undefined
|
|
2446
|
+
}
|
|
2447
|
+
profileOutcome = 'ran'
|
|
2410
2448
|
const refuse = (why: string): never => { throw new Error(`${id} — assemblyProfile: ${why}`) }
|
|
2411
2449
|
if (corpusTexts(assemblyProfile).every(text => text.trim() === '')) refuse('`files` has no non-empty file')
|
|
2412
2450
|
let thrower: unknown
|
|
@@ -2421,24 +2459,38 @@ function transformMacroImpl(
|
|
|
2421
2459
|
},
|
|
2422
2460
|
})
|
|
2423
2461
|
})
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2462
|
+
// ponytail: callback source is evaluated in global scope, so the corpus run sees
|
|
2463
|
+
// `scope` as globals for this one synchronous call, and nothing can interleave with
|
|
2464
|
+
// it. Async work a callback starts that outlives the call no longer sees them;
|
|
2465
|
+
// rebind each callback instead if a helper ever needs that.
|
|
2466
|
+
const scope = assemblyProfile.scope ?? {}
|
|
2467
|
+
const names = Object.keys(scope)
|
|
2468
|
+
for (const name of names) {
|
|
2469
|
+
if (name in globalThis) refuse(`\`scope\` name "${name}" is already a global; a callback couldn't tell which one it reads`)
|
|
2470
|
+
}
|
|
2471
|
+
try {
|
|
2472
|
+
for (const name of names) Object.defineProperty(globalThis, name, { value: scope[name], configurable: true })
|
|
2473
|
+
return countSiteVisits(compiled.prog, rules => {
|
|
2474
|
+
const entry = rules[assemblyProfile.entry]!
|
|
2475
|
+
corpusTexts(assemblyProfile).forEach((text, i) => {
|
|
2476
|
+
const file = assemblyProfile.files[i]!
|
|
2477
|
+
if (text.trim() === '') return
|
|
2478
|
+
let result
|
|
2479
|
+
try { result = run(entry, text) } catch (e) {
|
|
2480
|
+
const source = String(thrower).replace(/\s+/g, ' ').slice(0, 200)
|
|
2481
|
+
throw new Error(`${id} — assemblyProfile: ${thrower === undefined ? 'the parse' : `gate or reducer \`${source}\``} threw on ${file}: ${String(e)}`, { cause: e })
|
|
2482
|
+
}
|
|
2483
|
+
if (!result.ok || result.unconsumedFrom !== null) {
|
|
2484
|
+
refuse(`${file} does not parse from rule "${assemblyProfile.entry}": it stops at offset ${result.ok ? result.unconsumedFrom : result.span.end}`)
|
|
2485
|
+
}
|
|
2486
|
+
if (result.errors.length > 0) {
|
|
2487
|
+
refuse(`${file} parses only by recovering from ${result.errors.length} error(s), first at offset ${result.errors[0]!.span.start}`)
|
|
2488
|
+
}
|
|
2489
|
+
})
|
|
2490
|
+
}, fns)
|
|
2491
|
+
} finally {
|
|
2492
|
+
for (const name of names) Reflect.deleteProperty(globalThis, name)
|
|
2493
|
+
}
|
|
2442
2494
|
}
|
|
2443
2495
|
/** The fused table's replacement, with its selective default assembly if any. */
|
|
2444
2496
|
const assembledReplacement = (compiled: CompiledRuleMapTable, metadata: string, select: ReadonlySet<number>): string =>
|
|
@@ -3321,5 +3373,6 @@ function transformMacroImpl(
|
|
|
3321
3373
|
code: ms.toString(),
|
|
3322
3374
|
map: ms.generateMap({ hires: true }),
|
|
3323
3375
|
warnings,
|
|
3376
|
+
...(profileOutcome === undefined ? {} : { assemblyProfile: profileOutcome }),
|
|
3324
3377
|
}
|
|
3325
3378
|
}
|
package/src/version.ts
CHANGED