@caretcms/caretize 0.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 (102) hide show
  1. package/README.md +70 -0
  2. package/dist/backup.d.ts +30 -0
  3. package/dist/backup.d.ts.map +1 -0
  4. package/dist/backup.js +89 -0
  5. package/dist/backup.js.map +1 -0
  6. package/dist/bind-collection.d.ts +56 -0
  7. package/dist/bind-collection.d.ts.map +1 -0
  8. package/dist/bind-collection.js +140 -0
  9. package/dist/bind-collection.js.map +1 -0
  10. package/dist/bind-route.d.ts +40 -0
  11. package/dist/bind-route.d.ts.map +1 -0
  12. package/dist/bind-route.js +150 -0
  13. package/dist/bind-route.js.map +1 -0
  14. package/dist/cli-args.d.ts +30 -0
  15. package/dist/cli-args.d.ts.map +1 -0
  16. package/dist/cli-args.js +118 -0
  17. package/dist/cli-args.js.map +1 -0
  18. package/dist/cli.d.ts +11 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +356 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/detect.d.ts +76 -0
  23. package/dist/detect.d.ts.map +1 -0
  24. package/dist/detect.js +237 -0
  25. package/dist/detect.js.map +1 -0
  26. package/dist/discover.d.ts +13 -0
  27. package/dist/discover.d.ts.map +1 -0
  28. package/dist/discover.js +84 -0
  29. package/dist/discover.js.map +1 -0
  30. package/dist/frontmatter.d.ts +26 -0
  31. package/dist/frontmatter.d.ts.map +1 -0
  32. package/dist/frontmatter.js +52 -0
  33. package/dist/frontmatter.js.map +1 -0
  34. package/dist/identifiers.d.ts +26 -0
  35. package/dist/identifiers.d.ts.map +1 -0
  36. package/dist/identifiers.js +34 -0
  37. package/dist/identifiers.js.map +1 -0
  38. package/dist/import-wrap.d.ts +56 -0
  39. package/dist/import-wrap.d.ts.map +1 -0
  40. package/dist/import-wrap.js +149 -0
  41. package/dist/import-wrap.js.map +1 -0
  42. package/dist/index.d.ts +18 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +18 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/name.d.ts +49 -0
  47. package/dist/name.d.ts.map +1 -0
  48. package/dist/name.js +164 -0
  49. package/dist/name.js.map +1 -0
  50. package/dist/output.d.ts +30 -0
  51. package/dist/output.d.ts.map +1 -0
  52. package/dist/output.js +110 -0
  53. package/dist/output.js.map +1 -0
  54. package/dist/parse.d.ts +86 -0
  55. package/dist/parse.d.ts.map +1 -0
  56. package/dist/parse.js +92 -0
  57. package/dist/parse.js.map +1 -0
  58. package/dist/plan.d.ts +46 -0
  59. package/dist/plan.d.ts.map +1 -0
  60. package/dist/plan.js +76 -0
  61. package/dist/plan.js.map +1 -0
  62. package/dist/preflight.d.ts +19 -0
  63. package/dist/preflight.d.ts.map +1 -0
  64. package/dist/preflight.js +95 -0
  65. package/dist/preflight.js.map +1 -0
  66. package/dist/prop-hoist.d.ts +67 -0
  67. package/dist/prop-hoist.d.ts.map +1 -0
  68. package/dist/prop-hoist.js +232 -0
  69. package/dist/prop-hoist.js.map +1 -0
  70. package/dist/props.d.ts +38 -0
  71. package/dist/props.d.ts.map +1 -0
  72. package/dist/props.js +116 -0
  73. package/dist/props.js.map +1 -0
  74. package/dist/report.d.ts +42 -0
  75. package/dist/report.d.ts.map +1 -0
  76. package/dist/report.js +35 -0
  77. package/dist/report.js.map +1 -0
  78. package/dist/resolve.d.ts +15 -0
  79. package/dist/resolve.d.ts.map +1 -0
  80. package/dist/resolve.js +35 -0
  81. package/dist/resolve.js.map +1 -0
  82. package/dist/run.d.ts +52 -0
  83. package/dist/run.d.ts.map +1 -0
  84. package/dist/run.js +213 -0
  85. package/dist/run.js.map +1 -0
  86. package/dist/splice.d.ts +43 -0
  87. package/dist/splice.d.ts.map +1 -0
  88. package/dist/splice.js +90 -0
  89. package/dist/splice.js.map +1 -0
  90. package/dist/usage.d.ts +90 -0
  91. package/dist/usage.d.ts.map +1 -0
  92. package/dist/usage.js +249 -0
  93. package/dist/usage.js.map +1 -0
  94. package/dist/wrap.d.ts +72 -0
  95. package/dist/wrap.d.ts.map +1 -0
  96. package/dist/wrap.js +170 -0
  97. package/dist/wrap.js.map +1 -0
  98. package/dist/write.d.ts +28 -0
  99. package/dist/write.d.ts.map +1 -0
  100. package/dist/write.js +37 -0
  101. package/dist/write.js.map +1 -0
  102. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @caretcms/caretize
2
+
3
+ Make an existing Astro site editable with [CaretCMS](https://github.com/web-stacked/caretcms): `caretize` scans your `.astro` templates and interactively inserts `data-caret` attributes (plus `editable()` wraps for data arrays), turning static markup into inline-editable content — no schema or config rewrite required.
4
+
5
+ The static HTML you already have *is* the seed content: CaretCMS renders the original text until an editor saves an override, so caretize only has to mark what's editable.
6
+
7
+ ```sh
8
+ # in your Astro project root — preview first, write nothing
9
+ npx @caretcms/caretize --dry-run
10
+
11
+ # then run the interactive review
12
+ npx @caretcms/caretize
13
+ ```
14
+
15
+ ## What it does
16
+
17
+ For every candidate it finds, caretize shows the element and proposed binding and asks:
18
+
19
+ ```
20
+ src/pages/index.astro
21
+ <h1> "Launch faster with Acme"
22
+ + data-caret="pages::home::launch_faster_with_acme" (high)
23
+ [a]ccept [s]kip [e]dit [A]ll [S]kip-file [q]uit >
24
+ ```
25
+
26
+ - **Tags** pure-text leaf elements (`h1`–`h6`, `p`, `li`, `a`, `button`, …) and `<img>` `src` — exactly the set the CaretCMS rewrite engine can render, nothing more.
27
+ - **Names fields from content** (`"Our Programs 🎨"` → `our_programs`); prose tags keep role names. Names are deterministic and never collide with existing bindings, so re-runs are idempotent.
28
+ - **Wraps data arrays** (`const faqs = [...]` → `editable("pages::home::faqs", [...])`) so list content is editable too.
29
+ - **Flags what it can't tag** (loops, expressions, mixed markup) and tells you which flag unlocks each.
30
+
31
+ ## Safety model
32
+
33
+ - **Nothing is written until the review ends.** Every output is verified in memory first: it must re-parse as valid Astro and be a pure insertion of the original (your bytes survive untouched, in order).
34
+ - **All-or-nothing:** if any file fails verification, the run aborts having written nothing.
35
+ - **Backups:** every written file is first copied to `.caret/.caretize-bak/`; `caretize --restore` reverts the most recent run. Git is the real undo — caretize warns when your tree is dirty.
36
+ - Multibyte-safe: offsets are computed and spliced on UTF-8 buffers (emoji/em-dashes can't corrupt a tag).
37
+
38
+ ## Options
39
+
40
+ ```
41
+ caretize [path] [options]
42
+
43
+ path file or directory to scan (default: src/)
44
+ --dry-run print the plan, write nothing
45
+ -y, --yes auto-accept all suggestions at/above min-confidence
46
+ --min-confidence <lvl> high (default) | medium | low
47
+ --no-images skip <img> elements
48
+ --no-props skip hoisting static component-prop strings to editable()
49
+ --bind-collections bind getCollection().map() loops in place — a leaf
50
+ element rendering {item.data.field} gets a per-row
51
+ data-caret (direct-render only; props stay flagged)
52
+ --bind-routes bind a dynamic collection-detail route ([slug].astro)
53
+ to its current entry via getStaticPaths props
54
+ --rich also tag mixed-content blocks whose markup is
55
+ sanitizer-safe inline formatting (data-caret-rich)
56
+ --scope <collection::id> override the inferred scope (validated against the
57
+ runtime grammar: ^[a-z][a-z0-9_-]*$ :: ^[a-z0-9][a-z0-9_-]*$)
58
+ --report <file> write a JSON report
59
+ --restore restore the most recent backup, then exit
60
+ ```
61
+
62
+ ## After caretize
63
+
64
+ 1. `npm run dev`
65
+ 2. Sign in at `/admin` — with no `CARET_EDIT_PASSWORD` set, a temporary dev password is printed in your terminal
66
+ 3. Click any tagged element to edit it in place; the Studio lives at `/admin/cms`
67
+
68
+ Requires [`@caretcms/core`](https://www.npmjs.com/package/@caretcms/core) wired into `astro.config.mjs` (caretize's preflight checks this and tells you if it isn't).
69
+
70
+ MIT © CaretCMS contributors
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Backups + restore. Before any file is modified, its current bytes are copied
3
+ * to `.caret/.caretize-bak/<relpath>.<timestamp>.bak`. `--restore` rolls the
4
+ * most recent run back.
5
+ *
6
+ * The timestamp is injected (not read from the clock here) so a single run uses
7
+ * one consistent stamp across all files and the logic stays deterministic/
8
+ * testable.
9
+ */
10
+ /** Copy the current file to its timestamped backup, creating dirs as needed. */
11
+ export declare function writeBackup(rootDir: string, relPath: string, stamp: string): string;
12
+ interface BackupEntry {
13
+ relPath: string;
14
+ stamp: string;
15
+ backupAbs: string;
16
+ }
17
+ /** All backups under the backup root, parsed into {relPath, stamp}. */
18
+ export declare function listBackups(rootDir: string): BackupEntry[];
19
+ /** Restore an explicit set of backups (the files one run actually wrote).
20
+ * Used for failure rollback: restoring "the latest stamp" instead could
21
+ * resurrect a PREVIOUS run's backups over files the user has since edited,
22
+ * when the failing run dies before writing its own first backup. */
23
+ export declare function restoreBackups(rootDir: string, entries: ReadonlyArray<{
24
+ relPath: string;
25
+ backupAbs: string;
26
+ }>): string[];
27
+ /** Restore every file captured under the most recent run. Returns restored paths. */
28
+ export declare function restoreLatest(rootDir: string): string[];
29
+ export {};
30
+ //# sourceMappingURL=backup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../src/backup.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuBH,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMnF;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAoB1D;AASD;;;qEAGqE;AACrE,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,aAAa,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7D,MAAM,EAAE,CAQV;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAWvD"}
package/dist/backup.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Backups + restore. Before any file is modified, its current bytes are copied
3
+ * to `.caret/.caretize-bak/<relpath>.<timestamp>.bak`. `--restore` rolls the
4
+ * most recent run back.
5
+ *
6
+ * The timestamp is injected (not read from the clock here) so a single run uses
7
+ * one consistent stamp across all files and the logic stays deterministic/
8
+ * testable.
9
+ */
10
+ import { mkdirSync, copyFileSync, readdirSync, statSync, existsSync, copyFileSync as cp, } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ const BAK_DIR = join(".caret", ".caretize-bak");
13
+ function backupRoot(rootDir) {
14
+ return resolve(rootDir, BAK_DIR);
15
+ }
16
+ /** Absolute backup path for a project-relative file at a given timestamp. */
17
+ function backupPathFor(rootDir, relPath, stamp) {
18
+ return join(backupRoot(rootDir), `${relPath}.${stamp}.bak`);
19
+ }
20
+ /** Copy the current file to its timestamped backup, creating dirs as needed. */
21
+ export function writeBackup(rootDir, relPath, stamp) {
22
+ const src = resolve(rootDir, relPath);
23
+ const dest = backupPathFor(rootDir, relPath, stamp);
24
+ mkdirSync(dirname(dest), { recursive: true });
25
+ copyFileSync(src, dest);
26
+ return dest;
27
+ }
28
+ const BAK_RE = /^(.*)\.([0-9TZ:.\-]+)\.bak$/;
29
+ /** All backups under the backup root, parsed into {relPath, stamp}. */
30
+ export function listBackups(rootDir) {
31
+ const root = backupRoot(rootDir);
32
+ if (!existsSync(root))
33
+ return [];
34
+ const out = [];
35
+ const walk = (dir) => {
36
+ for (const name of readdirSync(dir)) {
37
+ const abs = join(dir, name);
38
+ if (statSync(abs).isDirectory()) {
39
+ walk(abs);
40
+ continue;
41
+ }
42
+ const m = BAK_RE.exec(name);
43
+ if (!m)
44
+ continue;
45
+ const relFromRoot = abs.slice(root.length + 1);
46
+ const relPath = relFromRoot.slice(0, relFromRoot.length - name.length) + m[1];
47
+ out.push({ relPath: relPath.split("\\").join("/"), stamp: m[2], backupAbs: abs });
48
+ }
49
+ };
50
+ walk(root);
51
+ return out;
52
+ }
53
+ /** The most recent timestamp present in the backup dir, or null. */
54
+ function latestStamp(rootDir) {
55
+ const stamps = listBackups(rootDir).map((b) => b.stamp);
56
+ if (stamps.length === 0)
57
+ return null;
58
+ return stamps.sort().at(-1) ?? null;
59
+ }
60
+ /** Restore an explicit set of backups (the files one run actually wrote).
61
+ * Used for failure rollback: restoring "the latest stamp" instead could
62
+ * resurrect a PREVIOUS run's backups over files the user has since edited,
63
+ * when the failing run dies before writing its own first backup. */
64
+ export function restoreBackups(rootDir, entries) {
65
+ const restored = [];
66
+ for (const entry of entries) {
67
+ if (!existsSync(entry.backupAbs))
68
+ continue;
69
+ cp(entry.backupAbs, resolve(rootDir, entry.relPath));
70
+ restored.push(entry.relPath);
71
+ }
72
+ return restored;
73
+ }
74
+ /** Restore every file captured under the most recent run. Returns restored paths. */
75
+ export function restoreLatest(rootDir) {
76
+ const stamp = latestStamp(rootDir);
77
+ if (!stamp)
78
+ return [];
79
+ const restored = [];
80
+ for (const entry of listBackups(rootDir)) {
81
+ if (entry.stamp !== stamp)
82
+ continue;
83
+ const dest = resolve(rootDir, entry.relPath);
84
+ cp(entry.backupAbs, dest);
85
+ restored.push(entry.relPath);
86
+ }
87
+ return restored;
88
+ }
89
+ //# sourceMappingURL=backup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.js","sourceRoot":"","sources":["../src/backup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Tier-5 (--bind-collections): make `getCollection().map(...)` loops editable.
3
+ *
4
+ * Where the other tiers flag a fetched-data loop as "consider a dynamic
5
+ * collection", this binds the DIRECT-RENDER case in place: a leaf element whose
6
+ * only dynamic content is `{item.data.field}` gets a dynamic data-caret:
7
+ *
8
+ * <h2>{post.data.title}</h2>
9
+ * → <h2 data-caret={`blog::${post.id}::title`}>{post.data.title}</h2>
10
+ *
11
+ * Astro renders the template literal to the concrete `blog::<id>::title` per
12
+ * iteration, so the inline editor binds each row to its own entry. This is a
13
+ * PURE attribute insertion (the same splice + re-parse gate as the data-caret
14
+ * tag pass) — no rewrite of the map callback, nothing deleted, fully invertible.
15
+ *
16
+ * Scope (v1): only leaf elements rendering exactly one `item.data.X` and nothing
17
+ * else. The component-prop case (`<Card title={post.data.title}/>`) needs
18
+ * cross-file resolution and stays a flag.
19
+ */
20
+ import { type AstroNode, type TagNode } from "./parse.js";
21
+ export interface CollectionBindTarget {
22
+ /** Element opening-tag start offset — where the data-caret attribute splices in. */
23
+ startOffset: number;
24
+ /** Attribute text to insert, e.g. ``data-caret={`blog::${post.id}::title`}``. */
25
+ attribute: string;
26
+ collection: string;
27
+ field: string;
28
+ tag: string;
29
+ /** The loop variable (`posts` in `posts.map(...)`) — lets the CLI suppress the
30
+ * "consider a dynamic collection" flag for a loop this tier just covered. */
31
+ receiver: string;
32
+ /** `"loop"` for a getCollection().map() row (--bind-collections); `"route"` for
33
+ * the current entry of a dynamic detail page (--bind-routes). Only affects how
34
+ * the dry-run plan labels the bind. */
35
+ kind: "loop" | "route";
36
+ }
37
+ export declare const GET_COLLECTION_RE: RegExp;
38
+ /** Concatenated JS text of an expression node's direct text children. */
39
+ export declare function expressionJs(node: AstroNode): string;
40
+ export declare function hasCaretAttr(node: TagNode): boolean;
41
+ /** Does this expression introduce an iterator-callback param named `name`?
42
+ * `team.map((post) => …)` rebinds `post` for its subtree — elements inside
43
+ * render the iterated row, not whatever outer binding shares the name, so a
44
+ * binder holding `name` must treat the subtree as out of scope. */
45
+ export declare function iteratorParamShadows(js: string, name: string): boolean;
46
+ /** If `el` is a leaf element whose only dynamic content is `{<param>.data.<field>}`
47
+ * (no literal text, no child elements, exactly one expression), return the field.
48
+ * Shared with the route binder (`--bind-routes`), which resolves the same
49
+ * single-entry leaf shape from a dynamic detail page's entry variable. */
50
+ export declare function soleDataField(el: TagNode, param: string): string | null;
51
+ /**
52
+ * Detect direct-render collection-loop bindings. Returns one target per leaf
53
+ * element rendering `item.data.field` inside a `getCollection(...).map(...)`.
54
+ */
55
+ export declare function detectCollectionBindTargets(source: string, ast: AstroNode): CollectionBindTarget[];
56
+ //# sourceMappingURL=bind-collection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-collection.d.ts","sourceRoot":"","sources":["../src/bind-collection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAKrE,MAAM,WAAW,oBAAoB;IACnC,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ;kFAC8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB;;4CAEwC;IACxC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAWD,eAAO,MAAM,iBAAiB,QAC2F,CAAC;AAK1H,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAMpD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAInD;AAED;;;oEAGoE;AACpE,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAItE;AAED;;;2EAG2E;AAC3E,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBvE;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,oBAAoB,EAAE,CA2DlG"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tier-5 (--bind-collections): make `getCollection().map(...)` loops editable.
3
+ *
4
+ * Where the other tiers flag a fetched-data loop as "consider a dynamic
5
+ * collection", this binds the DIRECT-RENDER case in place: a leaf element whose
6
+ * only dynamic content is `{item.data.field}` gets a dynamic data-caret:
7
+ *
8
+ * <h2>{post.data.title}</h2>
9
+ * → <h2 data-caret={`blog::${post.id}::title`}>{post.data.title}</h2>
10
+ *
11
+ * Astro renders the template literal to the concrete `blog::<id>::title` per
12
+ * iteration, so the inline editor binds each row to its own entry. This is a
13
+ * PURE attribute insertion (the same splice + re-parse gate as the data-caret
14
+ * tag pass) — no rewrite of the map callback, nothing deleted, fully invertible.
15
+ *
16
+ * Scope (v1): only leaf elements rendering exactly one `item.data.X` and nothing
17
+ * else. The component-prop case (`<Card title={post.data.title}/>`) needs
18
+ * cross-file resolution and stays a flag.
19
+ */
20
+ import { isTagNode } from "./parse.js";
21
+ import { frontmatterRange } from "./frontmatter.js";
22
+ import { isRewritableTextTag } from "./detect.js";
23
+ import { escapeRe } from "./identifiers.js";
24
+ // `const posts = await getCollection('releases')`, including a chained
25
+ // initializer like `(await getCollection('x')).sort(...)`. The receiver must be
26
+ // the declaration the call actually initializes: an earlier unbounded-lazy form
27
+ // (`=\s*…[\s\S]*?getCollection`) let a PRECEDING declaration steal the capture,
28
+ // binding the wrong variable to the collection — a corrupt permanent storage
29
+ // key. So the initializer must START with the (optionally parenthesized,
30
+ // optionally awaited) call. Indirect shapes (`sortBy(await getCollection(…))`)
31
+ // now yield no binding — a silent miss is safe, a wrong receiver is not.
32
+ // Shared with the route binder (`--bind-routes`).
33
+ export const GET_COLLECTION_RE = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?\(?\s*(?:await\s+)?getCollection\(\s*['"]([^'"]+)['"]\s*\)/g;
34
+ // `posts.map((post) => ...` — receiver + the iteration parameter.
35
+ const MAP_RE = /([A-Za-z_$][\w$]*)\s*\.\s*map\s*\(\s*\(?\s*([A-Za-z_$][\w$]*)/;
36
+ /** Concatenated JS text of an expression node's direct text children. */
37
+ export function expressionJs(node) {
38
+ let out = "";
39
+ for (const child of node.children ?? []) {
40
+ if (child.type === "text")
41
+ out += child.value ?? "";
42
+ }
43
+ return out;
44
+ }
45
+ export function hasCaretAttr(node) {
46
+ return node.attributes?.some((a) => a.name === "data-caret" || a.name === "data-caret-rich") ?? false;
47
+ }
48
+ /** Does this expression introduce an iterator-callback param named `name`?
49
+ * `team.map((post) => …)` rebinds `post` for its subtree — elements inside
50
+ * render the iterated row, not whatever outer binding shares the name, so a
51
+ * binder holding `name` must treat the subtree as out of scope. */
52
+ export function iteratorParamShadows(js, name) {
53
+ return new RegExp(`\\.\\s*(?:map|filter|forEach|flatMap|reduce)\\s*\\(\\s*\\(?\\s*${escapeRe(name)}(?![\\w$])`).test(js);
54
+ }
55
+ /** If `el` is a leaf element whose only dynamic content is `{<param>.data.<field>}`
56
+ * (no literal text, no child elements, exactly one expression), return the field.
57
+ * Shared with the route binder (`--bind-routes`), which resolves the same
58
+ * single-entry leaf shape from a dynamic detail page's entry variable. */
59
+ export function soleDataField(el, param) {
60
+ let field = null;
61
+ for (const child of el.children ?? []) {
62
+ if (child.type === "text") {
63
+ if (child.value?.trim())
64
+ return null; // mixed literal text
65
+ continue;
66
+ }
67
+ if (child.type === "expression") {
68
+ if (field)
69
+ return null; // more than one expression
70
+ const m = new RegExp(`^${param}\\.data\\.([A-Za-z_$][\\w$]*)$`).exec(expressionJs(child).trim());
71
+ if (!m)
72
+ return null;
73
+ field = m[1];
74
+ continue;
75
+ }
76
+ return null; // a nested element/component — not a leaf text element
77
+ }
78
+ return field;
79
+ }
80
+ /**
81
+ * Detect direct-render collection-loop bindings. Returns one target per leaf
82
+ * element rendering `item.data.field` inside a `getCollection(...).map(...)`.
83
+ */
84
+ export function detectCollectionBindTargets(source, ast) {
85
+ const fm = frontmatterRange(source);
86
+ if (!fm)
87
+ return [];
88
+ const collections = new Map(); // receiver var -> collection name
89
+ const fmText = source.slice(fm.start, fm.end);
90
+ GET_COLLECTION_RE.lastIndex = 0;
91
+ let gm;
92
+ while ((gm = GET_COLLECTION_RE.exec(fmText)))
93
+ collections.set(gm[1], gm[2]);
94
+ if (collections.size === 0)
95
+ return [];
96
+ const targets = [];
97
+ const visit = (node, ctx) => {
98
+ let nextCtx = ctx;
99
+ if (node.type === "expression") {
100
+ const js = expressionJs(node);
101
+ const mm = MAP_RE.exec(js);
102
+ const collection = mm && collections.get(mm[1]);
103
+ if (collection && mm) {
104
+ nextCtx = { collection, param: mm[2], receiver: mm[1] };
105
+ }
106
+ else if (ctx && iteratorParamShadows(js, ctx.param)) {
107
+ // A nested non-collection loop reuses the active param name
108
+ // (`team.map((post) => …)` inside a `posts.map((post) => …)`): inside
109
+ // it, `post` is a team row. Binding there would write to the wrong
110
+ // collection — drop the context for this subtree.
111
+ nextCtx = null;
112
+ }
113
+ }
114
+ if (nextCtx &&
115
+ isTagNode(node) &&
116
+ node.type === "element" &&
117
+ !hasCaretAttr(node) &&
118
+ isRewritableTextTag(node.name)) {
119
+ const field = soleDataField(node, nextCtx.param);
120
+ const startOffset = node.position?.start.offset ?? -1;
121
+ if (field && startOffset >= 0) {
122
+ const value = "`" + nextCtx.collection + "::${" + nextCtx.param + ".id}::" + field + "`";
123
+ targets.push({
124
+ startOffset,
125
+ attribute: "data-caret={" + value + "}",
126
+ collection: nextCtx.collection,
127
+ field,
128
+ tag: node.name,
129
+ receiver: nextCtx.receiver,
130
+ kind: "loop",
131
+ });
132
+ }
133
+ }
134
+ for (const child of node.children ?? [])
135
+ visit(child, nextCtx);
136
+ };
137
+ visit(ast, null);
138
+ return targets;
139
+ }
140
+ //# sourceMappingURL=bind-collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-collection.js","sourceRoot":"","sources":["../src/bind-collection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Tier-6 (--bind-routes): make a collection DETAIL page (dynamic route) editable.
3
+ *
4
+ * A dynamic route like `src/pages/blog/[slug].astro` renders ONE entry, sourced
5
+ * from `getStaticPaths` props. `deriveScope` skips it ("dynamic-route" — a
6
+ * template has no single static identity), so the tag pass never touches the
7
+ * per-entry fields that are the whole point of the page. This binds each leaf
8
+ * `{entry.data.field}` to the CURRENT entry with a dynamic data-caret — the same
9
+ * per-iteration template-literal trick as `--bind-collections`, but resolved
10
+ * through the `getStaticPaths → props → Astro.props` chain instead of a `.map()`
11
+ * callback:
12
+ *
13
+ * export async function getStaticPaths() {
14
+ * const posts = await getCollection('blog');
15
+ * return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
16
+ * }
17
+ * const { post } = Astro.props;
18
+ * ...
19
+ * <h1>{post.data.title}</h1>
20
+ * → <h1 data-caret={`blog::${post.id}::title`}>{post.data.title}</h1>
21
+ *
22
+ * Astro renders the template literal to the concrete `blog::<id>::title` for the
23
+ * page being built, so each detail page binds to its own entry. PURE attribute
24
+ * insertion — same splice + re-parse gate as the tag pass, nothing deleted.
25
+ *
26
+ * Scope (v1): the single-collection `getStaticPaths`-props shape, leaf elements
27
+ * rendering exactly one `entry.data.field`. Anything we can't resolve
28
+ * unambiguously (multiple collections in one `getStaticPaths`, an entry fetched
29
+ * via `getEntry()` / `Astro.params`, a component-prop field) yields no binding —
30
+ * never guess at a permanent storage key.
31
+ */
32
+ import { type AstroNode } from "./parse.js";
33
+ import { type CollectionBindTarget } from "./bind-collection.js";
34
+ /**
35
+ * Detect current-entry bindings on a dynamic collection-detail route. Returns one
36
+ * target per leaf element rendering `entry.data.field`, or `[]` when the route's
37
+ * entry source can't be resolved unambiguously.
38
+ */
39
+ export declare function detectRouteBindTargets(source: string, ast: AstroNode): CollectionBindTarget[];
40
+ //# sourceMappingURL=bind-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-route.d.ts","sourceRoot":"","sources":["../src/bind-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvD,OAAO,EACL,KAAK,oBAAoB,EAM1B,MAAM,sBAAsB,CAAC;AAmE9B;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,GACb,oBAAoB,EAAE,CA6CxB"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Tier-6 (--bind-routes): make a collection DETAIL page (dynamic route) editable.
3
+ *
4
+ * A dynamic route like `src/pages/blog/[slug].astro` renders ONE entry, sourced
5
+ * from `getStaticPaths` props. `deriveScope` skips it ("dynamic-route" — a
6
+ * template has no single static identity), so the tag pass never touches the
7
+ * per-entry fields that are the whole point of the page. This binds each leaf
8
+ * `{entry.data.field}` to the CURRENT entry with a dynamic data-caret — the same
9
+ * per-iteration template-literal trick as `--bind-collections`, but resolved
10
+ * through the `getStaticPaths → props → Astro.props` chain instead of a `.map()`
11
+ * callback:
12
+ *
13
+ * export async function getStaticPaths() {
14
+ * const posts = await getCollection('blog');
15
+ * return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
16
+ * }
17
+ * const { post } = Astro.props;
18
+ * ...
19
+ * <h1>{post.data.title}</h1>
20
+ * → <h1 data-caret={`blog::${post.id}::title`}>{post.data.title}</h1>
21
+ *
22
+ * Astro renders the template literal to the concrete `blog::<id>::title` for the
23
+ * page being built, so each detail page binds to its own entry. PURE attribute
24
+ * insertion — same splice + re-parse gate as the tag pass, nothing deleted.
25
+ *
26
+ * Scope (v1): the single-collection `getStaticPaths`-props shape, leaf elements
27
+ * rendering exactly one `entry.data.field`. Anything we can't resolve
28
+ * unambiguously (multiple collections in one `getStaticPaths`, an entry fetched
29
+ * via `getEntry()` / `Astro.params`, a component-prop field) yields no binding —
30
+ * never guess at a permanent storage key.
31
+ */
32
+ import { isTagNode } from "./parse.js";
33
+ import { frontmatterRange } from "./frontmatter.js";
34
+ import { GET_COLLECTION_RE, expressionJs, hasCaretAttr, iteratorParamShadows, soleDataField, } from "./bind-collection.js";
35
+ import { isRewritableTextTag } from "./detect.js";
36
+ // `posts.map((post) => ...` — receiver + the iteration parameter.
37
+ const MAP_PARAM_RE = /([A-Za-z_$][\w$]*)\s*\.\s*map\s*\(\s*\(?\s*([A-Za-z_$][\w$]*)/g;
38
+ /** A standalone identifier inside an object/destructure body (shorthand key). */
39
+ function shorthand(name) {
40
+ return new RegExp(`(?:^|[{,\\s])${name}(?:\\s*[,}]|\\s*$)`);
41
+ }
42
+ /**
43
+ * Resolve the template's current-entry variable and its collection from the
44
+ * frontmatter, or null when the shape isn't the supported single-entry route.
45
+ */
46
+ function resolveEntryBinding(fmText) {
47
+ // 1. collections declared anywhere in the frontmatter (getStaticPaths included).
48
+ const collections = new Map(); // receiver var -> collection name
49
+ GET_COLLECTION_RE.lastIndex = 0;
50
+ let gm;
51
+ while ((gm = GET_COLLECTION_RE.exec(fmText)))
52
+ collections.set(gm[1], gm[2]);
53
+ if (collections.size === 0)
54
+ return null;
55
+ // 2. the map parameter tied to a collection receiver: posts.map((post) => ...).
56
+ // Bail on any ambiguity (two collections mapped) — never guess.
57
+ let collection = null;
58
+ let param = null;
59
+ MAP_PARAM_RE.lastIndex = 0;
60
+ let mm;
61
+ while ((mm = MAP_PARAM_RE.exec(fmText))) {
62
+ const col = collections.get(mm[1]);
63
+ if (!col)
64
+ continue;
65
+ if (collection && (collection !== col || param !== mm[2]))
66
+ return null;
67
+ collection = col;
68
+ param = mm[2];
69
+ }
70
+ if (!collection || !param)
71
+ return null;
72
+ // 3. which prop key carries the entry? props: { entry: post } | props: { post }.
73
+ const propsObj = /\bprops\s*:\s*\{([^}]*)\}/.exec(fmText);
74
+ if (!propsObj)
75
+ return null;
76
+ const propsInner = propsObj[1];
77
+ const explicit = new RegExp(`([A-Za-z_$][\\w$]*)\\s*:\\s*${param}\\b`).exec(propsInner);
78
+ const propKey = explicit
79
+ ? explicit[1]
80
+ : shorthand(param).test(propsInner)
81
+ ? param
82
+ : null;
83
+ if (!propKey)
84
+ return null;
85
+ // 4. the template variable: const { <propKey> } = Astro.props (rename allowed).
86
+ const destruct = /\bconst\s*\{([^}]*)\}\s*=\s*Astro\.props/.exec(fmText);
87
+ if (!destruct)
88
+ return null;
89
+ const dInner = destruct[1];
90
+ const rename = new RegExp(`\\b${propKey}\\s*:\\s*([A-Za-z_$][\\w$]*)`).exec(dInner);
91
+ const entryVar = rename
92
+ ? rename[1]
93
+ : shorthand(propKey).test(dInner)
94
+ ? propKey
95
+ : null;
96
+ if (!entryVar)
97
+ return null;
98
+ return { collection, entryVar };
99
+ }
100
+ /**
101
+ * Detect current-entry bindings on a dynamic collection-detail route. Returns one
102
+ * target per leaf element rendering `entry.data.field`, or `[]` when the route's
103
+ * entry source can't be resolved unambiguously.
104
+ */
105
+ export function detectRouteBindTargets(source, ast) {
106
+ const fm = frontmatterRange(source);
107
+ if (!fm)
108
+ return [];
109
+ const resolved = resolveEntryBinding(source.slice(fm.start, fm.end));
110
+ if (!resolved)
111
+ return [];
112
+ const { collection, entryVar } = resolved;
113
+ const targets = [];
114
+ const visit = (node, shadowed) => {
115
+ let nextShadowed = shadowed;
116
+ // A template loop whose callback param reuses the entry variable's name
117
+ // (`{team.map((post) => <li>{post.data.name}</li>)}` on a page whose entry
118
+ // var is `post`) shadows it: inside, `post` is the iterated row. Binding
119
+ // there would write to the wrong collection — skip the whole subtree.
120
+ if (!nextShadowed && node.type === "expression" &&
121
+ iteratorParamShadows(expressionJs(node), entryVar)) {
122
+ nextShadowed = true;
123
+ }
124
+ if (!nextShadowed &&
125
+ isTagNode(node) &&
126
+ node.type === "element" &&
127
+ !hasCaretAttr(node) &&
128
+ isRewritableTextTag(node.name)) {
129
+ const field = soleDataField(node, entryVar);
130
+ const startOffset = node.position?.start.offset ?? -1;
131
+ if (field && startOffset >= 0) {
132
+ const value = "`" + collection + "::${" + entryVar + ".id}::" + field + "`";
133
+ targets.push({
134
+ startOffset,
135
+ attribute: "data-caret={" + value + "}",
136
+ collection,
137
+ field,
138
+ tag: node.name,
139
+ receiver: entryVar,
140
+ kind: "route",
141
+ });
142
+ }
143
+ }
144
+ for (const child of node.children ?? [])
145
+ visit(child, nextShadowed);
146
+ };
147
+ visit(ast, false);
148
+ return targets;
149
+ }
150
+ //# sourceMappingURL=bind-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-route.js","sourceRoot":"","sources":["../src/bind-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAEL,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,aAAa,GACd,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,kEAAkE;AAClE,MAAM,YAAY,GAAG,gEAAgE,CAAC;AAEtF,iFAAiF;AACjF,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,MAAM,CAAC,gBAAgB,IAAI,oBAAoB,CAAC,CAAC;AAC9D,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAC1B,MAAc;IAEd,iFAAiF;IACjF,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC,CAAC,kCAAkC;IACjF,iBAAiB,CAAC,SAAS,GAAG,CAAC,CAAC;IAChC,IAAI,EAA0B,CAAC;IAC/B,OAAO,CAAC,EAAE,GAAG,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAAE,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,WAAW,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAExC,gFAAgF;IAChF,mEAAmE;IACnE,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,YAAY,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3B,IAAI,EAA0B,CAAC;IAC/B,OAAO,CAAC,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,IAAI,UAAU,IAAI,CAAC,UAAU,KAAK,GAAG,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACvE,UAAU,GAAG,GAAG,CAAC;QACjB,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IAChB,CAAC;IACD,IAAI,CAAC,UAAU,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAEvC,iFAAiF;IACjF,MAAM,QAAQ,GAAG,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1D,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC/B,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,+BAA+B,KAAK,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACxF,MAAM,OAAO,GAAG,QAAQ;QACtB,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QACb,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;YACjC,CAAC,CAAC,KAAK;YACP,CAAC,CAAC,IAAI,CAAC;IACX,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,gFAAgF;IAChF,MAAM,QAAQ,GAAG,0CAA0C,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzE,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,OAAO,8BAA8B,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpF,MAAM,QAAQ,GAAG,MAAM;QACrB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACX,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;YAC/B,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,IAAI,CAAC;IACX,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAE3B,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAc,EACd,GAAc;IAEd,MAAM,EAAE,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,CAAC;IACnB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrE,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzB,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC;IAE1C,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,MAAM,KAAK,GAAG,CAAC,IAAe,EAAE,QAAiB,EAAQ,EAAE;QACzD,IAAI,YAAY,GAAG,QAAQ,CAAC;QAC5B,wEAAwE;QACxE,2EAA2E;QAC3E,yEAAyE;QACzE,sEAAsE;QACtE,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;YAC3C,oBAAoB,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC;YACvD,YAAY,GAAG,IAAI,CAAC;QACtB,CAAC;QAED,IACE,CAAC,YAAY;YACb,SAAS,CAAC,IAAI,CAAC;YACf,IAAI,CAAC,IAAI,KAAK,SAAS;YACvB,CAAC,YAAY,CAAC,IAAI,CAAC;YACnB,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAC9B,CAAC;YACD,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;YACtD,IAAI,KAAK,IAAI,WAAW,IAAI,CAAC,EAAE,CAAC;gBAC9B,MAAM,KAAK,GAAG,GAAG,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,GAAG,CAAC;gBAC5E,OAAO,CAAC,IAAI,CAAC;oBACX,WAAW;oBACX,SAAS,EAAE,cAAc,GAAG,KAAK,GAAG,GAAG;oBACvC,UAAU;oBACV,KAAK;oBACL,GAAG,EAAE,IAAI,CAAC,IAAI;oBACd,QAAQ,EAAE,QAAQ;oBAClB,IAAI,EAAE,OAAO;iBACd,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,IAAI,EAAE;YAAE,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IACtE,CAAC,CAAC;IACF,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAClB,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Argument parsing + help text for the caretize CLI. Pure and process-free: on a
3
+ * bad flag it throws `CliUsageError` rather than calling process.exit, so the
4
+ * parser is unit-testable and cli.ts owns the actual exit. Keeping this out of
5
+ * cli.ts also keeps that file a thin orchestration shell.
6
+ */
7
+ import type { Confidence } from "./detect.js";
8
+ import { type Scope } from "./name.js";
9
+ export interface Args {
10
+ target?: string;
11
+ dryRun: boolean;
12
+ yes: boolean;
13
+ minConfidence: Confidence;
14
+ noImages: boolean;
15
+ rich: boolean;
16
+ noProps: boolean;
17
+ bindCollections: boolean;
18
+ bindRoutes: boolean;
19
+ scope?: Scope;
20
+ report?: string;
21
+ restore: boolean;
22
+ help: boolean;
23
+ version: boolean;
24
+ }
25
+ /** Thrown on an invalid invocation; cli.ts maps it to a stderr message + exit 2. */
26
+ export declare class CliUsageError extends Error {
27
+ }
28
+ export declare const HELP = "caretize \u00B7 make an Astro project editable (data-caret + editable())\n\nUsage: caretize [path] [options]\n\n path file or directory to scan (default: src/)\n --dry-run print the plan, write nothing\n -y, --yes auto-accept all suggestions at/above min-confidence\n --min-confidence <lvl> high (default) | medium | low\n --no-images skip <img> elements\n --no-props skip hoisting static component-prop strings to editable()\n --bind-collections bind getCollection().map() loops in place \u2014 a leaf\n element rendering {item.data.field} gets a per-row\n data-caret (direct-render only; props stay flagged)\n --bind-routes bind a dynamic collection-detail route to the current\n entry \u2014 a leaf rendering {entry.data.field} (entry from\n getStaticPaths props) gets a data-caret instead of the\n page being skipped as a dynamic route\n --rich also tag mixed-content blocks whose markup is\n sanitizer-safe inline formatting (data-caret-rich)\n --scope <collection::id> override the inferred scope\n --report <file> write a JSON report\n --restore restore the most recent backup, then exit\n -h, --help show this help\n -v, --version print version\n";
29
+ export declare function parseArgs(argv: string[]): Args;
30
+ //# sourceMappingURL=cli-args.d.ts.map