payload-live-preview 2.0.6 → 2.1.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.
Files changed (116) hide show
  1. package/README.md +141 -38
  2. package/dist/adapters/astro/FragmentBridge.astro +25 -0
  3. package/dist/adapters/astro/PreviewBoundary.astro +2 -4
  4. package/dist/adapters/astro/RichText.astro +26 -5
  5. package/dist/adapters/astro/index.d.ts +41 -9
  6. package/dist/adapters/astro/index.js +1 -1
  7. package/dist/adapters/astro/index.js.map +1 -1
  8. package/dist/adapters/astro/middleware-entry.d.ts +3 -3
  9. package/dist/adapters/astro/middleware-entry.js +1 -1
  10. package/dist/adapters/astro/middleware-entry.js.map +1 -1
  11. package/dist/adapters/nextjs/index.d.ts +17 -12
  12. package/dist/adapters/nextjs/index.js +1 -1
  13. package/dist/adapters/nextjs/index.js.map +1 -1
  14. package/dist/adapters/nuxt/index.d.ts +11 -6
  15. package/dist/adapters/nuxt/index.js +1 -1
  16. package/dist/adapters/nuxt/index.js.map +1 -1
  17. package/dist/adapters/nuxt/module.d.ts +64 -13
  18. package/dist/adapters/nuxt/module.js +1 -1
  19. package/dist/adapters/nuxt/module.js.map +1 -1
  20. package/dist/adapters/react/index.d.ts +11 -7
  21. package/dist/adapters/react/index.js +1 -1
  22. package/dist/adapters/react/index.js.map +1 -1
  23. package/dist/adapters/sveltekit/index.d.ts +10 -5
  24. package/dist/adapters/sveltekit/index.js +1 -1
  25. package/dist/adapters/sveltekit/index.js.map +1 -1
  26. package/dist/adapters/vue/index.d.ts +10 -7
  27. package/dist/adapters/vue/index.js +1 -1
  28. package/dist/adapters/vue/index.js.map +1 -1
  29. package/dist/annotate.d.ts +15 -2
  30. package/dist/annotate.js.map +1 -1
  31. package/dist/client.cjs +1 -1
  32. package/dist/client.cjs.map +1 -1
  33. package/dist/client.d.cts +367 -266
  34. package/dist/client.d.ts +367 -266
  35. package/dist/client.js +1 -1
  36. package/dist/client.js.map +1 -1
  37. package/dist/codegen-cli.js.map +1 -1
  38. package/dist/codegen.cjs.map +1 -1
  39. package/dist/codegen.d.cts +7 -1
  40. package/dist/codegen.d.ts +7 -1
  41. package/dist/codegen.js.map +1 -1
  42. package/dist/core.cjs +1 -1
  43. package/dist/core.cjs.map +1 -1
  44. package/dist/core.d.cts +544 -392
  45. package/dist/core.d.ts +544 -392
  46. package/dist/core.js +1 -1
  47. package/dist/core.js.map +1 -1
  48. package/dist/doctor-cli.d.ts +1 -1
  49. package/dist/doctor-cli.js +1 -1
  50. package/dist/doctor-cli.js.map +1 -1
  51. package/dist/doctor.d.ts +2 -2
  52. package/dist/doctor.js +1 -1
  53. package/dist/doctor.js.map +1 -1
  54. package/dist/{document-session-BfBfLVpg.d.ts → document-session-DWi9H3Qk.d.ts} +7 -0
  55. package/dist/{fragment-endpoint-DATKU183.d.ts → fragment-endpoint-B2qcRw5T.d.ts} +22 -7
  56. package/dist/fragment.cjs +1 -1
  57. package/dist/fragment.cjs.map +1 -1
  58. package/dist/fragment.d.cts +37 -16
  59. package/dist/fragment.d.ts +37 -16
  60. package/dist/fragment.js +1 -1
  61. package/dist/fragment.js.map +1 -1
  62. package/dist/index.cjs +1 -1
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +598 -400
  65. package/dist/index.d.ts +598 -400
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/lean.cjs +1 -1
  69. package/dist/lean.cjs.map +1 -1
  70. package/dist/lean.d.cts +5 -2
  71. package/dist/lean.d.ts +5 -2
  72. package/dist/lean.js +1 -1
  73. package/dist/lean.js.map +1 -1
  74. package/dist/lexical.cjs +1 -1
  75. package/dist/lexical.cjs.map +1 -1
  76. package/dist/lexical.d.cts +32 -11
  77. package/dist/lexical.d.ts +32 -11
  78. package/dist/lexical.js +1 -1
  79. package/dist/lexical.js.map +1 -1
  80. package/dist/{locals-CNUGYbA_.d.ts → locals-B1mF3aTb.d.ts} +1 -1
  81. package/dist/{middleware-CWObDO37.d.ts → middleware-BGobLN9P.d.ts} +11 -5
  82. package/dist/migrate.js +1 -1
  83. package/dist/migrate.js.map +1 -1
  84. package/dist/{options-uHKrDM2U.d.ts → options-Cm8fC7dj.d.ts} +31 -1
  85. package/dist/payload.cjs +1 -1
  86. package/dist/payload.cjs.map +1 -1
  87. package/dist/payload.d.cts +18 -2
  88. package/dist/payload.d.ts +18 -2
  89. package/dist/payload.js +1 -1
  90. package/dist/payload.js.map +1 -1
  91. package/dist/plugin.cjs +2 -0
  92. package/dist/plugin.cjs.map +1 -0
  93. package/dist/plugin.d.cts +30 -0
  94. package/dist/plugin.d.ts +30 -0
  95. package/dist/plugin.js +2 -0
  96. package/dist/plugin.js.map +1 -0
  97. package/dist/plugins.cjs +1 -1
  98. package/dist/plugins.cjs.map +1 -1
  99. package/dist/plugins.d.cts +102 -29
  100. package/dist/plugins.d.ts +102 -29
  101. package/dist/plugins.js +1 -1
  102. package/dist/plugins.js.map +1 -1
  103. package/dist/{probe-Bzm4fYFL.d.ts → probe-DGPDsnbj.d.ts} +6 -2
  104. package/dist/server.cjs +1 -1
  105. package/dist/server.cjs.map +1 -1
  106. package/dist/server.d.cts +43 -4
  107. package/dist/server.d.ts +43 -4
  108. package/dist/server.js +1 -1
  109. package/dist/server.js.map +1 -1
  110. package/dist/structural.cjs +2 -2
  111. package/dist/structural.cjs.map +1 -1
  112. package/dist/structural.d.cts +61 -13
  113. package/dist/structural.d.ts +61 -13
  114. package/dist/structural.js +2 -2
  115. package/dist/structural.js.map +1 -1
  116. package/package.json +33 -4
@@ -1 +1 @@
1
- {"version":3,"file":"annotate.js","names":["ELEMENT","ONLY_INTERPOLATION","ANY_INTERPOLATION","MEMBER_CHAIN","FIELD_ATTRIBUTE","LOOP_VARIABLES","lineAt","source","index","line","position","charCodeAt","pathOf","chain","slice","indexOf","describe","expression","flat","replace","trim","length","refusalReason","paths","items","trimmed","test","path","root","has","fieldAttribute","candidate","HELPER","IMPORT_ALIAS","IMPORT_LINE","HELPER_LINE","FENCE","splitFrontmatter","code","startsWith","frontmatter","bodyOffset","end","withHelper","lines","isAstro","id","includes","endsWith","annotateSource","options","inventory","Set","entry","globals","collections","field","fields","add","annotatablePaths","body","candidates","refusals","names","pattern","match","matchAll","loopVariables","tag","attributes","content","only","exec","push","reason","insertAt","scanTemplate","shifted","map","gated","allowPublicBindings","rewritten","render","out","sort","a","b","applyAnnotations","livePreviewAnnotate","include","read","readFile","readFileSync","name","enforce","load","onRefusals"],"sources":["../src/codegen/annotate/scan.ts","../src/codegen/annotate/vite-plugin.ts","../src/codegen/annotate/index.ts"],"sourcesContent":["/**\n * Find the places in a template where a field value is printed on its own, and\n * decide — never guess — which of them can carry a binding.\n *\n * The shape this recognises is deliberately narrow, and it is the same in every\n * template language the package adapts:\n *\n * ```\n * <h1>{page.title}</h1> Astro, JSX, Svelte\n * ```\n *\n * An element whose entire content is one member expression, rooted at some\n * variable, whose remaining path is a field the schema actually has. Everything\n * else — an interpolation next to text, a call, a ternary, a path the schema\n * does not know, a component instead of an element — is reported instead, with\n * the reason. A wrong `data-payload-field` is worse than a missing one: it makes\n * the runtime write a value into markup that was never meant for it.\n *\n * Markup rather than an AST on purpose. Astro and Svelte templates are not\n * TypeScript, so ts-morph would only cover the JSX third of the problem, and\n * three parsers would be three chances to disagree about which shape is safe.\n * The shape above is unambiguous enough to scan for.\n */\n\n/** A place that can carry a binding, with the attribute it should get. @internal */\nexport interface AnnotationCandidate {\n /** Offset where the attribute is inserted, inside the opening tag. */\n readonly insertAt: number;\n /** The binding path, as `data-payload-field` must spell it. */\n readonly path: string;\n /** 1-based line, for the report. */\n readonly line: number;\n readonly tag: string;\n}\n\n/** A place that looks like an output but was left alone, and why. */\nexport interface AnnotationRefusal {\n readonly line: number;\n readonly reason: string;\n /** The source of the interpolation, trimmed, for the report. */\n readonly expression: string;\n}\n\nexport interface ScanResult {\n readonly candidates: readonly AnnotationCandidate[];\n readonly refusals: readonly AnnotationRefusal[];\n}\n\nexport interface ScanOptions {\n /** Field paths the schema has, exactly as a binding must spell them. */\n readonly paths: ReadonlySet<string>;\n}\n\n/**\n * `<tag attrs>content</tag>` with no element inside it, the closing tag\n * repeating the name. Only lowercase tags: an uppercase one is a component,\n * whose children are its own business.\n */\nconst ELEMENT = /<([a-z][a-z0-9-]*)((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([^<]*)<\\/\\1\\s*>/gu;\n\n/** The whole content is one interpolation, and nothing else. */\nconst ONLY_INTERPOLATION = /^\\s*\\{([^{}]*)\\}\\s*$/u;\n\n/** An interpolation somewhere in the content, which is a different case. */\nconst ANY_INTERPOLATION = /\\{([^{}]*)\\}/u;\n\n/** A plain member chain: `page.title`, `data.hero.eyebrow`. Nothing callable, nothing indexed. */\nconst MEMBER_CHAIN = /^[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)+$/u;\n\nconst FIELD_ATTRIBUTE = 'data-payload-field';\n\n/**\n * Where a template names one array item at a time: a `.map`, `.flatMap` or\n * `.forEach` callback's first parameter, a `for … of` variable, a Svelte\n * `{#each … as item}`. A chain rooted at such a name prints an item, and the\n * item's `title` is not the document's `title`, however alike the paths look.\n * A name bound this way anywhere in the file is treated as an item everywhere:\n * refusing one real field access costs a line in the report, binding an item\n * costs the preview.\n */\nconst LOOP_VARIABLES: readonly RegExp[] = [\n /\\.(?:map|flatMap|forEach)\\(\\s*(?:async\\s+)?\\(?\\s*([A-Za-z_$][\\w$]*)/gu,\n /\\bfor\\s*\\(\\s*(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s+of\\b/gu,\n /\\{#each\\s[^}]*?\\sas\\s+([A-Za-z_$][\\w$]*)/gu,\n];\n\nfunction loopVariables(source: string): ReadonlySet<string> {\n const names = new Set<string>();\n for (const pattern of LOOP_VARIABLES) {\n for (const match of source.matchAll(pattern)) {\n if (match[1] !== undefined) names.add(match[1]);\n }\n }\n return names;\n}\n\nfunction lineAt(source: string, index: number): number {\n let line = 1;\n for (let position = 0; position < index; position += 1) {\n if (source.charCodeAt(position) === 10) line += 1;\n }\n return line;\n}\n\n/**\n * The binding path a chain would address: everything after the root variable.\n * `page.hero.title` → `hero.title`, and a lone `title` is not a chain at all —\n * a bare identifier says nothing about which document it came from.\n */\nfunction pathOf(chain: string): string {\n return chain.slice(chain.indexOf('.') + 1);\n}\n\nfunction describe(expression: string): string {\n const flat = expression.replace(/\\s+/gu, ' ').trim();\n return flat.length > 60 ? `${flat.slice(0, 57)}…` : flat;\n}\n\n/** Why this interpolation cannot become a binding, in the words of the report. */\nfunction refusalReason(\n expression: string,\n paths: ReadonlySet<string>,\n items: ReadonlySet<string>,\n): string | undefined {\n const trimmed = expression.trim();\n if (trimmed.length === 0) return 'the element is empty';\n if (!MEMBER_CHAIN.test(trimmed)) {\n return 'not a plain field access — a call, an operator or an index cannot be traced to one field';\n }\n const path = pathOf(trimmed);\n const root = trimmed.slice(0, trimmed.indexOf('.'));\n if (items.has(root)) {\n return `\\`${root}\\` is a loop item — a binding names a field of the document, and \\`${path}\\` there is a different value, or none`;\n }\n if (!paths.has(path)) {\n return `the schema has no field \\`${path}\\`; the binding would name something that never arrives`;\n }\n return undefined;\n}\n\nexport function scanTemplate(source: string, options: ScanOptions): ScanResult {\n const candidates: AnnotationCandidate[] = [];\n const refusals: AnnotationRefusal[] = [];\n const items = loopVariables(source);\n for (const match of source.matchAll(ELEMENT)) {\n const [, tag = '', attributes = '', content = ''] = match;\n const line = lineAt(source, match.index);\n if (attributes.includes(FIELD_ATTRIBUTE)) continue;\n const only = ONLY_INTERPOLATION.exec(content);\n if (only === null) {\n // A value printed beside a label — `Published {page.publishedAt}`. The\n // runtime writes an element's whole text, so a binding here would eat the\n // label; splitting the markup is a decision for whoever wrote it.\n const beside = ANY_INTERPOLATION.exec(content);\n if (beside !== null) {\n refusals.push({\n line,\n reason:\n 'the value is printed beside other content — a binding replaces the whole text, so the markup has to be split first',\n expression: describe(content),\n });\n }\n continue;\n }\n const expression = only[1] ?? '';\n const reason = refusalReason(expression, options.paths, items);\n if (reason !== undefined) {\n refusals.push({ line, reason, expression: describe(expression) });\n continue;\n }\n candidates.push({\n // After `<tag`, before whatever the element already carries.\n insertAt: match.index + 1 + tag.length,\n path: pathOf(expression.trim()),\n line,\n tag,\n });\n }\n return { candidates, refusals };\n}\n\n/** The attribute a candidate becomes: what `pll-codegen annotate` writes into a file. */\nexport function fieldAttribute(candidate: AnnotationCandidate): string {\n return `${FIELD_ATTRIBUTE}=\"${candidate.path}\"`;\n}\n\n/**\n * Apply the candidates back-to-front, so every earlier offset still holds.\n *\n * `render` decides what a candidate becomes, because the two callers write\n * different things from the same decision: the codemod writes the attribute\n * into the file a person reads, and the build-time annotator writes a call to\n * the authorization-bound helper. What may be annotated at all is decided in\n * one place — above — and never here.\n */\nexport function applyAnnotations(\n source: string,\n candidates: readonly AnnotationCandidate[],\n render: (candidate: AnnotationCandidate) => string = fieldAttribute,\n): string {\n let out = source;\n for (const candidate of [...candidates].sort((a, b) => b.insertAt - a.insertAt)) {\n out = `${out.slice(0, candidate.insertAt)} ${render(candidate)}${out.slice(candidate.insertAt)}`;\n }\n return out;\n}\n","/**\n * `livePreviewAnnotate()` — the annotator as a build step, so a template needs\n * no binding attributes written into it by hand.\n *\n * It rewrites the same shape `pll-codegen annotate` rewrites, decided by the\n * same scanner (`./scan`), into a call rather than a fixed attribute:\n *\n * ```astro\n * <h1>{page.title}</h1>\n * → <h1 {...__lpPreview.bind('title')}>{page.title}</h1>\n * ```\n *\n * The difference is who may see it. A fixed attribute is in the HTML of every\n * visitor and describes the content model to all of them; the call resolves per\n * request against the authorization the adapter published, so an unauthorized\n * response carries no `data-payload-*` at all (ADR 0006). A static build has no\n * request to resolve against and therefore emits nothing — `allowPublicBindings`\n * is how you say you want the attribute anyway, out loud.\n *\n * Astro only, for a reason rather than a lack of time: the rewrite needs a\n * template whose own scope can reach the request context, which frontmatter and\n * `Astro.locals` give and a Svelte or Vue component does not — there the value\n * would have to travel through `load` or a payload, where a function cannot go.\n *\n * Vite compatibility is a recorded fact, not a guess: `name`, `enforce` and\n * `load(id)` are the whole surface used here, and all three have been stable\n * since Vite 5. The majors that matter are in `quality/compat-matrix.json`,\n * which is what the tests read.\n *\n * `load` rather than `transform`, which is where a source rewrite would\n * normally go. Astro compiles `.astro` in a `transform` of its own, registered\n * `enforce: 'pre'` and ahead of anything a config contributes, so by the time\n * any user transform runs the markup is gone — measured, not assumed: a probe\n * plugin at `pre` received compiled output importing from Astro's own compiler\n * runtime, not the template. Every `load` runs before every `transform`, so\n * that is where the file is still a template. Reading it is what Rollup's\n * default loader would have done anyway.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { annotatablePaths, type AnnotatableSchema } from './index';\nimport { applyAnnotations, fieldAttribute, scanTemplate, type AnnotationRefusal } from './scan';\n\n/** The helper the rewritten markup calls, and the import that defines it. */\nconst HELPER = '__lpPreview';\nconst IMPORT_ALIAS = '__lpPreviewBindingsFromLocals';\nconst IMPORT_LINE = `import { previewBindingsFromLocals as ${IMPORT_ALIAS} } from 'payload-live-preview/server';`;\nconst HELPER_LINE = `const ${HELPER} = ${IMPORT_ALIAS}(Astro.locals);`;\nconst FENCE = '---';\n\nexport interface AnnotatePluginOptions {\n /** The schema, from `generateTypes().inventory` or the `--inventory` file. */\n readonly inventory: AnnotatableSchema;\n /**\n * Write the plain `data-payload-field` attribute instead of the gated call.\n * A statically built site has no request to authorize, so this is the only\n * way it can carry bindings — and it publishes the field names to everyone.\n * Default `false`.\n */\n readonly allowPublicBindings?: boolean;\n /** Which modules to transform. Default: every `.astro` file. */\n readonly include?: (id: string) => boolean;\n /** Called once per file with what the scanner would not annotate. */\n readonly onRefusals?: (file: string, refusals: readonly AnnotationRefusal[]) => void;\n /** Reads a template. Defaults to the file system; injected by tests. */\n readonly readFile?: (path: string) => string;\n}\n\n/** The slice of Vite's plugin shape this uses; three hooks, stable since Vite 5. */\nexport interface AnnotateVitePlugin {\n readonly name: string;\n readonly enforce: 'pre';\n load(id: string): { code: string; map: null } | null;\n}\n\n/** Everything before the second `---`, and where the template starts. @internal */\nexport function splitFrontmatter(code: string): {\n readonly frontmatter: string;\n readonly bodyOffset: number;\n} {\n if (!code.startsWith(FENCE)) return { frontmatter: '', bodyOffset: 0 };\n const end = code.indexOf(`\\n${FENCE}`, FENCE.length);\n if (end === -1) return { frontmatter: '', bodyOffset: 0 };\n const bodyOffset = end + 1 + FENCE.length;\n return { frontmatter: code.slice(FENCE.length, end), bodyOffset };\n}\n\n/** The frontmatter with the helper in it, whether or not the file had one. */\nfunction withHelper(code: string, bodyOffset: number): string {\n const lines = `${IMPORT_LINE}\\n${HELPER_LINE}\\n`;\n if (bodyOffset === 0) return `${FENCE}\\n${lines}${FENCE}\\n${code}`;\n return `${FENCE}\\n${lines}${code.slice(FENCE.length, bodyOffset)}${code.slice(bodyOffset)}`;\n}\n\n/**\n * A plain `.astro` path. Not a sub-request (`?astro&type=style`), not a virtual\n * module (`\\0…`): those are Astro's own, and loading them is not this plugin's\n * business.\n */\nconst isAstro = (id: string): boolean =>\n !id.startsWith('\\0') && !id.includes('?') && id.endsWith('.astro');\n\n/**\n * The transform on its own, so a test can call it without a bundler and a\n * caller can see exactly what a file becomes.\n * @internal\n */\nexport function annotateSource(\n code: string,\n options: AnnotatePluginOptions,\n): { readonly code: string; readonly refusals: readonly AnnotationRefusal[] } {\n const paths = annotatablePaths(options.inventory);\n const { bodyOffset } = splitFrontmatter(code);\n // Frontmatter is TypeScript, not markup: scanning it would match a template\n // written inside a string, and annotate something no browser ever sees.\n const body = code.slice(bodyOffset);\n const { candidates, refusals } = scanTemplate(body, { paths });\n if (candidates.length === 0) return { code, refusals };\n\n const shifted = candidates.map((candidate) => ({\n ...candidate,\n insertAt: candidate.insertAt + bodyOffset,\n }));\n const gated = options.allowPublicBindings !== true;\n const rewritten = applyAnnotations(code, shifted, (candidate) =>\n gated ? `{...${HELPER}.bind('${candidate.path}')}` : fieldAttribute(candidate),\n );\n return {\n code: gated ? withHelper(rewritten, bodyOffset) : rewritten,\n refusals,\n };\n}\n\n/**\n * ```js\n * // astro.config.mjs\n * vite: { plugins: [livePreviewAnnotate({ inventory })] }\n * ```\n */\nexport function livePreviewAnnotate(options: AnnotatePluginOptions): AnnotateVitePlugin {\n const include = options.include ?? isAstro;\n const read = options.readFile ?? ((path: string) => readFileSync(path, 'utf8'));\n return {\n name: 'payload-live-preview:annotate',\n enforce: 'pre',\n load(id) {\n if (!include(id)) return null;\n const source = read(id);\n const { code, refusals } = annotateSource(source, options);\n if (refusals.length > 0) options.onRefusals?.(id, refusals);\n // The unannotated source is still returned: this loader replaces the\n // default one, so declining would leave the module empty.\n return { code, map: null };\n },\n };\n}\n","/**\n * `pll-codegen annotate`: put `data-payload-field` where a template already\n * prints a field, and report every place it would have had to guess.\n *\n * The schema decides what exists — the same inventory `--inventory` writes, so\n * an annotation and a runtime binding spell a path the same way. What the tool\n * cannot attribute is listed, never invented: an unbound field costs an editor\n * one invisible edit, a wrongly bound one writes a value into the wrong element\n * on every keystroke.\n */\n\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { relative } from 'node:path';\nimport type { PreviewInventory } from '../inventory';\nimport {\n applyAnnotations,\n scanTemplate,\n type AnnotationCandidate,\n type AnnotationRefusal,\n} from './scan';\n\nexport type { AnnotationCandidate, AnnotationRefusal } from './scan';\n\n/** Templates this scanner understands; a `.vue` template uses `{{ … }}` and is not one yet. @internal */\nexport const ANNOTATABLE_EXTENSIONS: readonly string[] = ['.astro', '.jsx', '.tsx', '.svelte'];\n\n/** @internal */\nexport interface AnnotateFileResult {\n readonly file: string;\n /** The annotated source; identical to the input when nothing was added. */\n readonly code: string;\n readonly annotations: readonly AnnotationCandidate[];\n readonly refusals: readonly AnnotationRefusal[];\n readonly changed: boolean;\n}\n\n/** @internal */\nexport interface AnnotateResult {\n readonly files: readonly AnnotateFileResult[];\n readonly annotationCount: number;\n readonly refusalCount: number;\n /** Whether the files were written; `false` is a dry run. */\n readonly written: boolean;\n}\n\n/** @internal */\nexport interface AnnotateOptions {\n /** Template files to read. */\n readonly files: readonly string[];\n /** The schema's addressable paths, from `generateTypes().inventory`. */\n readonly inventory: PreviewInventory;\n /** Write the annotated sources back. Without it nothing is touched. */\n readonly write?: boolean;\n /** Resolves the paths in the report. Defaults to `process.cwd()`. */\n readonly cwd?: string;\n /** Reader/writer injection for tests. */\n readonly io?: {\n readonly read: (path: string) => Promise<string>;\n readonly write: (path: string, content: string) => Promise<void>;\n };\n}\n\n/**\n * The part of an inventory the annotator reads: the paths, and nothing else.\n *\n * `PreviewInventory` from `generateTypes()` satisfies it, and so does an object\n * a project writes by hand. Naming the minimum keeps the build plugin's public\n * surface free of the extracted-field union behind the full inventory — a dozen\n * types a consumer of a Vite plugin has no reason to meet.\n */\nexport interface AnnotatableSchema {\n readonly globals: readonly { readonly fields: readonly { readonly path: string }[] }[];\n readonly collections: readonly { readonly fields: readonly { readonly path: string }[] }[];\n}\n\n/**\n * Every path the schema can address, in one set.\n *\n * An array's items are spelled `slides.*.caption` in the inventory because that\n * is how the runtime resolves them; a template writes `slide.caption` inside a\n * loop, where nothing connects `slide` to `slides`. Those paths are therefore\n * kept out: the scanner would otherwise match a name that only looks right.\n * @internal\n */\nexport function annotatablePaths(inventory: AnnotatableSchema): ReadonlySet<string> {\n const paths = new Set<string>();\n for (const entry of [...inventory.globals, ...inventory.collections]) {\n for (const field of entry.fields) {\n if (!field.path.includes('*')) paths.add(field.path);\n }\n }\n return paths;\n}\n\n/** @internal */\nexport async function annotateTemplates(options: AnnotateOptions): Promise<AnnotateResult> {\n const read = options.io?.read ?? ((path: string) => readFile(path, 'utf8'));\n const write =\n options.io?.write ?? ((path: string, content: string) => writeFile(path, content, 'utf8'));\n const root = options.cwd ?? process.cwd();\n const paths = annotatablePaths(options.inventory);\n const files: AnnotateFileResult[] = [];\n\n for (const file of options.files) {\n const source = await read(file);\n const { candidates, refusals } = scanTemplate(source, { paths });\n const code = candidates.length === 0 ? source : applyAnnotations(source, candidates);\n const changed = code !== source;\n if (changed && options.write === true) await write(file, code);\n files.push({\n file: relative(root, file) || file,\n code,\n annotations: candidates,\n refusals,\n changed,\n });\n }\n\n return {\n files,\n annotationCount: files.reduce((total, file) => total + file.annotations.length, 0),\n refusalCount: files.reduce((total, file) => total + file.refusals.length, 0),\n written: options.write === true,\n };\n}\n\n/** The report, one line per annotation and per refusal, in file order. @internal */\nexport function formatAnnotateReport(result: AnnotateResult): string {\n const lines: string[] = [];\n for (const file of result.files) {\n if (file.annotations.length === 0 && file.refusals.length === 0) continue;\n lines.push(file.file);\n for (const annotation of file.annotations) {\n lines.push(` ${String(annotation.line)}: <${annotation.tag}> → ${annotation.path}`);\n }\n for (const refusal of file.refusals) {\n lines.push(\n ` ${String(refusal.line)}: left alone — ${refusal.reason} (${refusal.expression})`,\n );\n }\n }\n const verb = result.written ? 'annotated' : 'would annotate';\n lines.push(\n `${verb} ${String(result.annotationCount)} element(s); ` +\n `${String(result.refusalCount)} left for a human`,\n );\n return `${lines.join('\\n')}\\n`;\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"annotate.js","names":["ELEMENT","ONLY_INTERPOLATION","ANY_INTERPOLATION","MEMBER_CHAIN","FIELD_ATTRIBUTE","LOOP_VARIABLES","lineAt","source","index","line","position","charCodeAt","pathOf","chain","slice","indexOf","describe","expression","flat","replace","trim","length","refusalReason","paths","items","trimmed","test","path","root","has","fieldAttribute","candidate","HELPER","IMPORT_ALIAS","IMPORT_LINE","HELPER_LINE","FENCE","splitFrontmatter","code","startsWith","frontmatter","bodyOffset","end","withHelper","lines","isAstro","id","includes","endsWith","annotateSource","options","inventory","Set","entry","globals","collections","field","fields","add","annotatablePaths","body","candidates","refusals","names","pattern","match","matchAll","loopVariables","tag","attributes","content","only","exec","push","reason","insertAt","scanTemplate","shifted","map","gated","allowPublicBindings","rewritten","render","out","sort","a","b","applyAnnotations","livePreviewAnnotate","include","read","readFile","readFileSync","name","enforce","load","onRefusals"],"sources":["../src/codegen/annotate/scan.ts","../src/codegen/annotate/vite-plugin.ts","../src/codegen/annotate/index.ts"],"sourcesContent":["/**\n * Find the places in a template where a field value is printed on its own, and\n * decide — never guess — which of them can carry a binding.\n *\n * The shape this recognises is deliberately narrow, and it is the same in every\n * template language the package adapts:\n *\n * ```\n * <h1>{page.title}</h1> Astro, JSX, Svelte\n * ```\n *\n * An element whose entire content is one member expression, rooted at some\n * variable, whose remaining path is a field the schema actually has. Everything\n * else — an interpolation next to text, a call, a ternary, a path the schema\n * does not know, a component instead of an element — is reported instead, with\n * the reason. A wrong `data-payload-field` is worse than a missing one: it makes\n * the runtime write a value into markup that was never meant for it.\n *\n * Markup rather than an AST on purpose. Astro and Svelte templates are not\n * TypeScript, so ts-morph would only cover the JSX third of the problem, and\n * three parsers would be three chances to disagree about which shape is safe.\n * The shape above is unambiguous enough to scan for.\n */\n\n/** A place that can carry a binding, with the attribute it should get. @internal */\nexport interface AnnotationCandidate {\n /** Offset where the attribute is inserted, inside the opening tag. */\n readonly insertAt: number;\n /** The binding path, as `data-payload-field` must spell it. */\n readonly path: string;\n /** 1-based line, for the report. */\n readonly line: number;\n readonly tag: string;\n}\n\n/**\n * A place that looks like an output but was left alone, and why.\n *\n * @beta\n */\nexport interface AnnotationRefusal {\n readonly line: number;\n readonly reason: string;\n /** The source of the interpolation, trimmed, for the report. */\n readonly expression: string;\n}\n\nexport interface ScanResult {\n readonly candidates: readonly AnnotationCandidate[];\n readonly refusals: readonly AnnotationRefusal[];\n}\n\nexport interface ScanOptions {\n /** Field paths the schema has, exactly as a binding must spell them. */\n readonly paths: ReadonlySet<string>;\n}\n\n/**\n * `<tag attrs>content</tag>` with no element inside it, the closing tag\n * repeating the name. Only lowercase tags: an uppercase one is a component,\n * whose children are its own business.\n */\nconst ELEMENT = /<([a-z][a-z0-9-]*)((?:\"[^\"]*\"|'[^']*'|[^>\"'])*)>([^<]*)<\\/\\1\\s*>/gu;\n\n/** The whole content is one interpolation, and nothing else. */\nconst ONLY_INTERPOLATION = /^\\s*\\{([^{}]*)\\}\\s*$/u;\n\n/** An interpolation somewhere in the content, which is a different case. */\nconst ANY_INTERPOLATION = /\\{([^{}]*)\\}/u;\n\n/** A plain member chain: `page.title`, `data.hero.eyebrow`. Nothing callable, nothing indexed. */\nconst MEMBER_CHAIN = /^[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)+$/u;\n\nconst FIELD_ATTRIBUTE = 'data-payload-field';\n\n/**\n * Where a template names one array item at a time: a `.map`, `.flatMap` or\n * `.forEach` callback's first parameter, a `for … of` variable, a Svelte\n * `{#each … as item}`. A chain rooted at such a name prints an item, and the\n * item's `title` is not the document's `title`, however alike the paths look.\n * A name bound this way anywhere in the file is treated as an item everywhere:\n * refusing one real field access costs a line in the report, binding an item\n * costs the preview.\n */\nconst LOOP_VARIABLES: readonly RegExp[] = [\n /\\.(?:map|flatMap|forEach)\\(\\s*(?:async\\s+)?\\(?\\s*([A-Za-z_$][\\w$]*)/gu,\n /\\bfor\\s*\\(\\s*(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s+of\\b/gu,\n /\\{#each\\s[^}]*?\\sas\\s+([A-Za-z_$][\\w$]*)/gu,\n];\n\nfunction loopVariables(source: string): ReadonlySet<string> {\n const names = new Set<string>();\n for (const pattern of LOOP_VARIABLES) {\n for (const match of source.matchAll(pattern)) {\n if (match[1] !== undefined) names.add(match[1]);\n }\n }\n return names;\n}\n\nfunction lineAt(source: string, index: number): number {\n let line = 1;\n for (let position = 0; position < index; position += 1) {\n if (source.charCodeAt(position) === 10) line += 1;\n }\n return line;\n}\n\n/**\n * The binding path a chain would address: everything after the root variable.\n * `page.hero.title` → `hero.title`, and a lone `title` is not a chain at all —\n * a bare identifier says nothing about which document it came from.\n */\nfunction pathOf(chain: string): string {\n return chain.slice(chain.indexOf('.') + 1);\n}\n\nfunction describe(expression: string): string {\n const flat = expression.replace(/\\s+/gu, ' ').trim();\n return flat.length > 60 ? `${flat.slice(0, 57)}…` : flat;\n}\n\n/** Why this interpolation cannot become a binding, in the words of the report. */\nfunction refusalReason(\n expression: string,\n paths: ReadonlySet<string>,\n items: ReadonlySet<string>,\n): string | undefined {\n const trimmed = expression.trim();\n if (trimmed.length === 0) return 'the element is empty';\n if (!MEMBER_CHAIN.test(trimmed)) {\n return 'not a plain field access — a call, an operator or an index cannot be traced to one field';\n }\n const path = pathOf(trimmed);\n const root = trimmed.slice(0, trimmed.indexOf('.'));\n if (items.has(root)) {\n return `\\`${root}\\` is a loop item — a binding names a field of the document, and \\`${path}\\` there is a different value, or none`;\n }\n if (!paths.has(path)) {\n return `the schema has no field \\`${path}\\`; the binding would name something that never arrives`;\n }\n return undefined;\n}\n\nexport function scanTemplate(source: string, options: ScanOptions): ScanResult {\n const candidates: AnnotationCandidate[] = [];\n const refusals: AnnotationRefusal[] = [];\n const items = loopVariables(source);\n for (const match of source.matchAll(ELEMENT)) {\n const [, tag = '', attributes = '', content = ''] = match;\n const line = lineAt(source, match.index);\n if (attributes.includes(FIELD_ATTRIBUTE)) continue;\n const only = ONLY_INTERPOLATION.exec(content);\n if (only === null) {\n // A value printed beside a label — `Published {page.publishedAt}`. The\n // runtime writes an element's whole text, so a binding here would eat the\n // label; splitting the markup is a decision for whoever wrote it.\n const beside = ANY_INTERPOLATION.exec(content);\n if (beside !== null) {\n refusals.push({\n line,\n reason:\n 'the value is printed beside other content — a binding replaces the whole text, so the markup has to be split first',\n expression: describe(content),\n });\n }\n continue;\n }\n const expression = only[1] ?? '';\n const reason = refusalReason(expression, options.paths, items);\n if (reason !== undefined) {\n refusals.push({ line, reason, expression: describe(expression) });\n continue;\n }\n candidates.push({\n // After `<tag`, before whatever the element already carries.\n insertAt: match.index + 1 + tag.length,\n path: pathOf(expression.trim()),\n line,\n tag,\n });\n }\n return { candidates, refusals };\n}\n\n/** The attribute a candidate becomes: what `pll-codegen annotate` writes into a file. */\nexport function fieldAttribute(candidate: AnnotationCandidate): string {\n return `${FIELD_ATTRIBUTE}=\"${candidate.path}\"`;\n}\n\n/**\n * Apply the candidates back-to-front, so every earlier offset still holds.\n *\n * `render` decides what a candidate becomes, because the two callers write\n * different things from the same decision: the codemod writes the attribute\n * into the file a person reads, and the build-time annotator writes a call to\n * the authorization-bound helper. What may be annotated at all is decided in\n * one place — above — and never here.\n */\nexport function applyAnnotations(\n source: string,\n candidates: readonly AnnotationCandidate[],\n render: (candidate: AnnotationCandidate) => string = fieldAttribute,\n): string {\n let out = source;\n for (const candidate of [...candidates].sort((a, b) => b.insertAt - a.insertAt)) {\n out = `${out.slice(0, candidate.insertAt)} ${render(candidate)}${out.slice(candidate.insertAt)}`;\n }\n return out;\n}\n","/**\n * `livePreviewAnnotate()` — the annotator as a build step, so a template needs\n * no binding attributes written into it by hand.\n *\n * It rewrites the same shape `pll-codegen annotate` rewrites, decided by the\n * same scanner (`./scan`), into a call rather than a fixed attribute:\n *\n * ```astro\n * <h1>{page.title}</h1>\n * → <h1 {...__lpPreview.bind('title')}>{page.title}</h1>\n * ```\n *\n * The difference is who may see it. A fixed attribute is in the HTML of every\n * visitor and describes the content model to all of them; the call resolves per\n * request against the authorization the adapter published, so an unauthorized\n * response carries no `data-payload-*` at all (ADR 0006). A static build has no\n * request to resolve against and therefore emits nothing — `allowPublicBindings`\n * is how you say you want the attribute anyway, out loud.\n *\n * Astro only, for a reason rather than a lack of time: the rewrite needs a\n * template whose own scope can reach the request context, which frontmatter and\n * `Astro.locals` give and a Svelte or Vue component does not — there the value\n * would have to travel through `load` or a payload, where a function cannot go.\n *\n * Vite compatibility is a recorded fact, not a guess: `name`, `enforce` and\n * `load(id)` are the whole surface used here, and all three have been stable\n * since Vite 5. The majors that matter are in `quality/compat-matrix.json`,\n * which is what the tests read.\n *\n * `load` rather than `transform`, which is where a source rewrite would\n * normally go. Astro compiles `.astro` in a `transform` of its own, registered\n * `enforce: 'pre'` and ahead of anything a config contributes, so by the time\n * any user transform runs the markup is gone — measured, not assumed: a probe\n * plugin at `pre` received compiled output importing from Astro's own compiler\n * runtime, not the template. Every `load` runs before every `transform`, so\n * that is where the file is still a template. Reading it is what Rollup's\n * default loader would have done anyway.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { annotatablePaths, type AnnotatableSchema } from './index';\nimport { applyAnnotations, fieldAttribute, scanTemplate, type AnnotationRefusal } from './scan';\n\n/** The helper the rewritten markup calls, and the import that defines it. */\nconst HELPER = '__lpPreview';\nconst IMPORT_ALIAS = '__lpPreviewBindingsFromLocals';\nconst IMPORT_LINE = `import { previewBindingsFromLocals as ${IMPORT_ALIAS} } from 'payload-live-preview/server';`;\nconst HELPER_LINE = `const ${HELPER} = ${IMPORT_ALIAS}(Astro.locals);`;\nconst FENCE = '---';\n\n/** @beta */\nexport interface AnnotatePluginOptions {\n /** The schema, from `generateTypes().inventory` or the `--inventory` file. */\n readonly inventory: AnnotatableSchema;\n /**\n * Write the plain `data-payload-field` attribute instead of the gated call.\n * A statically built site has no request to authorize, so this is the only\n * way it can carry bindings — and it publishes the field names to everyone.\n * Default `false`.\n */\n readonly allowPublicBindings?: boolean;\n /** Which modules to transform. Default: every `.astro` file. */\n readonly include?: (id: string) => boolean;\n /** Called once per file with what the scanner would not annotate. */\n readonly onRefusals?: (file: string, refusals: readonly AnnotationRefusal[]) => void;\n /** Reads a template. Defaults to the file system; injected by tests. */\n readonly readFile?: (path: string) => string;\n}\n\n/**\n * The slice of Vite's plugin shape this uses; three hooks, stable since Vite 5.\n *\n * @beta\n */\nexport interface AnnotateVitePlugin {\n readonly name: string;\n readonly enforce: 'pre';\n load(id: string): { code: string; map: null } | null;\n}\n\n/** Everything before the second `---`, and where the template starts. @internal */\nexport function splitFrontmatter(code: string): {\n readonly frontmatter: string;\n readonly bodyOffset: number;\n} {\n if (!code.startsWith(FENCE)) return { frontmatter: '', bodyOffset: 0 };\n const end = code.indexOf(`\\n${FENCE}`, FENCE.length);\n if (end === -1) return { frontmatter: '', bodyOffset: 0 };\n const bodyOffset = end + 1 + FENCE.length;\n return { frontmatter: code.slice(FENCE.length, end), bodyOffset };\n}\n\n/** The frontmatter with the helper in it, whether or not the file had one. */\nfunction withHelper(code: string, bodyOffset: number): string {\n const lines = `${IMPORT_LINE}\\n${HELPER_LINE}\\n`;\n if (bodyOffset === 0) return `${FENCE}\\n${lines}${FENCE}\\n${code}`;\n return `${FENCE}\\n${lines}${code.slice(FENCE.length, bodyOffset)}${code.slice(bodyOffset)}`;\n}\n\n/**\n * A plain `.astro` path. Not a sub-request (`?astro&type=style`), not a virtual\n * module (`\\0…`): those are Astro's own, and loading them is not this plugin's\n * business.\n */\nconst isAstro = (id: string): boolean =>\n !id.startsWith('\\0') && !id.includes('?') && id.endsWith('.astro');\n\n/**\n * The transform on its own, so a test can call it without a bundler and a\n * caller can see exactly what a file becomes.\n * @internal\n */\nexport function annotateSource(\n code: string,\n options: AnnotatePluginOptions,\n): { readonly code: string; readonly refusals: readonly AnnotationRefusal[] } {\n const paths = annotatablePaths(options.inventory);\n const { bodyOffset } = splitFrontmatter(code);\n // Frontmatter is TypeScript, not markup: scanning it would match a template\n // written inside a string, and annotate something no browser ever sees.\n const body = code.slice(bodyOffset);\n const { candidates, refusals } = scanTemplate(body, { paths });\n if (candidates.length === 0) return { code, refusals };\n\n const shifted = candidates.map((candidate) => ({\n ...candidate,\n insertAt: candidate.insertAt + bodyOffset,\n }));\n const gated = options.allowPublicBindings !== true;\n const rewritten = applyAnnotations(code, shifted, (candidate) =>\n gated ? `{...${HELPER}.bind('${candidate.path}')}` : fieldAttribute(candidate),\n );\n return {\n code: gated ? withHelper(rewritten, bodyOffset) : rewritten,\n refusals,\n };\n}\n\n/**\n * ```js\n * // astro.config.mjs\n * vite: { plugins: [livePreviewAnnotate({ inventory })] }\n * ```\n *\n * @beta\n */\nexport function livePreviewAnnotate(options: AnnotatePluginOptions): AnnotateVitePlugin {\n const include = options.include ?? isAstro;\n const read = options.readFile ?? ((path: string) => readFileSync(path, 'utf8'));\n return {\n name: 'payload-live-preview:annotate',\n enforce: 'pre',\n load(id) {\n if (!include(id)) return null;\n const source = read(id);\n const { code, refusals } = annotateSource(source, options);\n if (refusals.length > 0) options.onRefusals?.(id, refusals);\n // The unannotated source is still returned: this loader replaces the\n // default one, so declining would leave the module empty.\n return { code, map: null };\n },\n };\n}\n","/**\n * `pll-codegen annotate`: put `data-payload-field` where a template already\n * prints a field, and report every place it would have had to guess.\n *\n * The schema decides what exists — the same inventory `--inventory` writes, so\n * an annotation and a runtime binding spell a path the same way. What the tool\n * cannot attribute is listed, never invented: an unbound field costs an editor\n * one invisible edit, a wrongly bound one writes a value into the wrong element\n * on every keystroke.\n */\n\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { relative } from 'node:path';\nimport type { PreviewInventory } from '../inventory';\nimport {\n applyAnnotations,\n scanTemplate,\n type AnnotationCandidate,\n type AnnotationRefusal,\n} from './scan';\n\nexport type { AnnotationCandidate, AnnotationRefusal } from './scan';\n\n/** Templates this scanner understands; a `.vue` template uses `{{ … }}` and is not one yet. @internal */\nexport const ANNOTATABLE_EXTENSIONS: readonly string[] = ['.astro', '.jsx', '.tsx', '.svelte'];\n\n/** @internal */\nexport interface AnnotateFileResult {\n readonly file: string;\n /** The annotated source; identical to the input when nothing was added. */\n readonly code: string;\n readonly annotations: readonly AnnotationCandidate[];\n readonly refusals: readonly AnnotationRefusal[];\n readonly changed: boolean;\n}\n\n/** @internal */\nexport interface AnnotateResult {\n readonly files: readonly AnnotateFileResult[];\n readonly annotationCount: number;\n readonly refusalCount: number;\n /** Whether the files were written; `false` is a dry run. */\n readonly written: boolean;\n}\n\n/** @internal */\nexport interface AnnotateOptions {\n /** Template files to read. */\n readonly files: readonly string[];\n /** The schema's addressable paths, from `generateTypes().inventory`. */\n readonly inventory: PreviewInventory;\n /** Write the annotated sources back. Without it nothing is touched. */\n readonly write?: boolean;\n /** Resolves the paths in the report. Defaults to `process.cwd()`. */\n readonly cwd?: string;\n /** Reader/writer injection for tests. */\n readonly io?: {\n readonly read: (path: string) => Promise<string>;\n readonly write: (path: string, content: string) => Promise<void>;\n };\n}\n\n/**\n * The part of an inventory the annotator reads: the paths, and nothing else.\n *\n * `PreviewInventory` from `generateTypes()` satisfies it, and so does an object\n * a project writes by hand. Naming the minimum keeps the build plugin's public\n * surface free of the extracted-field union behind the full inventory — a dozen\n * types a consumer of a Vite plugin has no reason to meet.\n *\n * @beta\n */\nexport interface AnnotatableSchema {\n readonly globals: readonly { readonly fields: readonly { readonly path: string }[] }[];\n readonly collections: readonly { readonly fields: readonly { readonly path: string }[] }[];\n}\n\n/**\n * Every path the schema can address, in one set.\n *\n * An array's items are spelled `slides.*.caption` in the inventory because that\n * is how the runtime resolves them; a template writes `slide.caption` inside a\n * loop, where nothing connects `slide` to `slides`. Those paths are therefore\n * kept out: the scanner would otherwise match a name that only looks right.\n * @internal\n */\nexport function annotatablePaths(inventory: AnnotatableSchema): ReadonlySet<string> {\n const paths = new Set<string>();\n for (const entry of [...inventory.globals, ...inventory.collections]) {\n for (const field of entry.fields) {\n if (!field.path.includes('*')) paths.add(field.path);\n }\n }\n return paths;\n}\n\n/** @internal */\nexport async function annotateTemplates(options: AnnotateOptions): Promise<AnnotateResult> {\n const read = options.io?.read ?? ((path: string) => readFile(path, 'utf8'));\n const write =\n options.io?.write ?? ((path: string, content: string) => writeFile(path, content, 'utf8'));\n const root = options.cwd ?? process.cwd();\n const paths = annotatablePaths(options.inventory);\n const files: AnnotateFileResult[] = [];\n\n for (const file of options.files) {\n const source = await read(file);\n const { candidates, refusals } = scanTemplate(source, { paths });\n const code = candidates.length === 0 ? source : applyAnnotations(source, candidates);\n const changed = code !== source;\n if (changed && options.write === true) await write(file, code);\n files.push({\n file: relative(root, file) || file,\n code,\n annotations: candidates,\n refusals,\n changed,\n });\n }\n\n return {\n files,\n annotationCount: files.reduce((total, file) => total + file.annotations.length, 0),\n refusalCount: files.reduce((total, file) => total + file.refusals.length, 0),\n written: options.write === true,\n };\n}\n\n/** The report, one line per annotation and per refusal, in file order. @internal */\nexport function formatAnnotateReport(result: AnnotateResult): string {\n const lines: string[] = [];\n for (const file of result.files) {\n if (file.annotations.length === 0 && file.refusals.length === 0) continue;\n lines.push(file.file);\n for (const annotation of file.annotations) {\n lines.push(` ${String(annotation.line)}: <${annotation.tag}> → ${annotation.path}`);\n }\n for (const refusal of file.refusals) {\n lines.push(\n ` ${String(refusal.line)}: left alone — ${refusal.reason} (${refusal.expression})`,\n );\n }\n }\n const verb = result.written ? 'annotated' : 'would annotate';\n lines.push(\n `${verb} ${String(result.annotationCount)} element(s); ` +\n `${String(result.refusalCount)} left for a human`,\n );\n return `${lines.join('\\n')}\\n`;\n}\n"],"mappings":"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","ignoreList":[]}