astro-dev-edit 0.11.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/package.json +52 -0
  4. package/src/client/admin-bar.ts +622 -0
  5. package/src/client/api.ts +370 -0
  6. package/src/client/classify-cache.ts +61 -0
  7. package/src/client/css-inspect.ts +345 -0
  8. package/src/client/editors/asset-picker.ts +155 -0
  9. package/src/client/editors/body-editor.ts +419 -0
  10. package/src/client/editors/collections-panel.ts +1532 -0
  11. package/src/client/editors/copy-panel.ts +73 -0
  12. package/src/client/editors/drawer.ts +95 -0
  13. package/src/client/editors/entry.ts +433 -0
  14. package/src/client/editors/expression.ts +77 -0
  15. package/src/client/editors/fields.ts +309 -0
  16. package/src/client/editors/image.ts +268 -0
  17. package/src/client/editors/markup-insert.ts +73 -0
  18. package/src/client/editors/markup.ts +125 -0
  19. package/src/client/editors/media-grid.ts +326 -0
  20. package/src/client/editors/media-modal.ts +588 -0
  21. package/src/client/editors/notice.ts +160 -0
  22. package/src/client/editors/peek.ts +135 -0
  23. package/src/client/editors/settings-panel.ts +457 -0
  24. package/src/client/editors/source-popup.ts +166 -0
  25. package/src/client/editors/text.ts +105 -0
  26. package/src/client/editors/unsplash-pane.ts +317 -0
  27. package/src/client/element-context.ts +308 -0
  28. package/src/client/features.ts +81 -0
  29. package/src/client/focus.ts +166 -0
  30. package/src/client/group.ts +186 -0
  31. package/src/client/highlight.ts +146 -0
  32. package/src/client/hover.ts +485 -0
  33. package/src/client/icons.ts +160 -0
  34. package/src/client/markdown.ts +319 -0
  35. package/src/client/overlay.ts +466 -0
  36. package/src/client/page-source.ts +143 -0
  37. package/src/client/router.ts +198 -0
  38. package/src/client/shadow.ts +111 -0
  39. package/src/client/source-map.ts +150 -0
  40. package/src/client/state.ts +153 -0
  41. package/src/client/styles.ts +3485 -0
  42. package/src/client/tree-model.ts +45 -0
  43. package/src/client/tree.ts +366 -0
  44. package/src/client/ui.ts +987 -0
  45. package/src/client/unsplash-search.ts +250 -0
  46. package/src/index.ts +299 -0
  47. package/src/patcher/astro.ts +792 -0
  48. package/src/patcher/content-config.ts +1035 -0
  49. package/src/patcher/dotenv.ts +121 -0
  50. package/src/patcher/expression-trace.ts +326 -0
  51. package/src/patcher/frontmatter.ts +249 -0
  52. package/src/patcher/registry.ts +11 -0
  53. package/src/patcher/types.ts +32 -0
  54. package/src/server/annotate.ts +173 -0
  55. package/src/server/assets.ts +167 -0
  56. package/src/server/collection-entries.ts +91 -0
  57. package/src/server/content-config.ts +210 -0
  58. package/src/server/editor.ts +15 -0
  59. package/src/server/entry-detect.ts +110 -0
  60. package/src/server/entry-resolve-routes.ts +218 -0
  61. package/src/server/entry-routes.ts +304 -0
  62. package/src/server/inspect-locate.ts +81 -0
  63. package/src/server/inspect-routes.ts +94 -0
  64. package/src/server/middleware.ts +480 -0
  65. package/src/server/options.ts +778 -0
  66. package/src/server/page-source-routes.ts +71 -0
  67. package/src/server/paths.ts +219 -0
  68. package/src/server/private-files.ts +116 -0
  69. package/src/server/route-manifest.ts +200 -0
  70. package/src/server/router.ts +94 -0
  71. package/src/server/schema-introspect.ts +233 -0
  72. package/src/server/schema-routes.ts +808 -0
  73. package/src/server/settings-routes.ts +246 -0
  74. package/src/server/settings.ts +382 -0
  75. package/src/server/text-writes.ts +105 -0
  76. package/src/server/unsplash-routes.ts +515 -0
  77. package/src/server/zod-adapt.ts +239 -0
  78. package/src/shared/asset-path.ts +132 -0
  79. package/src/shared/protocol.ts +935 -0
  80. package/src/shared/slug.ts +17 -0
  81. package/src/shared/unsplash.ts +51 -0
@@ -0,0 +1,239 @@
1
+ /**
2
+ * zod-major adapter for schema introspection.
3
+ *
4
+ * Astro 5/6 ship zod v3; Astro 7 ships zod v4 (`astro/zod` re-exports
5
+ * `zod/v4`), which renamed every internal this introspector reads:
6
+ *
7
+ * - `_def.typeName` (`'ZodString'`) → `_def.type` (`'string'`)
8
+ * - enum values `_def.values` (array) → `_def.entries` (object map)
9
+ * - array element `_def.type` → `_def.element` — in v4 `_def.type` is the
10
+ * *string* `'array'`, so reading it as a schema silently yields nothing
11
+ * - literal `_def.value` → `_def.values[0]` (an array)
12
+ * - `.describe()` writes into `z.globalRegistry`, so `_def.description` is
13
+ * empty and only the `.description` getter answers
14
+ * - `.brand()` / `.refine()` no longer wrap at all (the kind stays `'string'`),
15
+ * while `.transform()` produces `pipe{in, out}` whose `out` is a `transform`
16
+ * node no widget can render — v4 follows `in`, the shape a form must produce.
17
+ * v3's `ZodPipeline` keeps following `out`, unchanged.
18
+ *
19
+ * Both majors are read **duck-typed** — we never import zod, so there is no
20
+ * dual-instance hazard and no peer dependency on it. Anything unrecognized
21
+ * yields `null` and the caller degrades to value inference rather than erroring.
22
+ */
23
+
24
+ export interface ZodNode {
25
+ _def?: Record<string, unknown> & { typeName?: unknown; type?: unknown };
26
+ /** Getter on both majors; v4's reads from the global metadata registry. */
27
+ description?: string;
28
+ /** Enum options getter, present on both majors. */
29
+ options?: unknown;
30
+ /** Array element getter, present on both majors. */
31
+ element?: unknown;
32
+ safeParse?: (v: unknown) => { success: boolean; error?: { issues?: { message: string }[] } };
33
+ }
34
+
35
+ export interface Unwrapped {
36
+ /** The terminal schema, with every wrapper peeled off. */
37
+ inner: ZodNode;
38
+ /** False when a wrapper made the field optional (optional/nullable/default/catch). */
39
+ required: boolean;
40
+ /** Resolved schema default, when one was declared. */
41
+ defaultValue: unknown;
42
+ }
43
+
44
+ export interface ZodAdapter {
45
+ major: 3 | 4;
46
+ /** Kind of a node, normalized lowercase: `'string'`, `'object'`, `'optional'`… */
47
+ kind(node: ZodNode): string | null;
48
+ /** Peel wrapper types down to the terminal one. */
49
+ unwrap(node: ZodNode): Unwrapped;
50
+ /** The node's `.describe()` text, if any. Read *after* unwrapping. */
51
+ description(node: ZodNode): string | undefined;
52
+ /** All-string enum values, or null when the enum isn't all strings. */
53
+ enumOptions(node: ZodNode): string[] | null;
54
+ /** An array's element schema. */
55
+ arrayElement(node: ZodNode): ZodNode | null;
56
+ /** A literal's value. */
57
+ literalValue(node: ZodNode): unknown;
58
+ /** An object schema's shape record. */
59
+ shape(node: ZodNode): Record<string, ZodNode> | null;
60
+ }
61
+
62
+ /** A wrapper kind, and which `_def` key holds the schema it wraps. */
63
+ interface Wrapper {
64
+ key: string;
65
+ /** This wrapper makes the field optional. */
66
+ optional?: true;
67
+ /** This wrapper makes the field required again, overriding inner optionality. */
68
+ pins?: true;
69
+ }
70
+
71
+ const V3_WRAPPERS: Record<string, Wrapper> = {
72
+ default: { key: 'innerType', optional: true },
73
+ optional: { key: 'innerType', optional: true },
74
+ nullable: { key: 'innerType', optional: true },
75
+ catch: { key: 'innerType', optional: true },
76
+ effects: { key: 'schema' },
77
+ // `ZodBranded` holds its inner schema on `_def.type`; `ZodReadonly` on
78
+ // `_def.innerType`. They are not interchangeable — reading `type` for
79
+ // readonly silently fails to unwrap and the field degrades to `json`.
80
+ branded: { key: 'type' },
81
+ readonly: { key: 'innerType' },
82
+ pipeline: { key: 'out' },
83
+ };
84
+
85
+ const V4_WRAPPERS: Record<string, Wrapper> = {
86
+ default: { key: 'innerType', optional: true },
87
+ prefault: { key: 'innerType', optional: true },
88
+ optional: { key: 'innerType', optional: true },
89
+ nullable: { key: 'innerType', optional: true },
90
+ catch: { key: 'innerType', optional: true },
91
+ readonly: { key: 'innerType' },
92
+ nonoptional: { key: 'innerType', pins: true },
93
+ pipe: { key: 'in' },
94
+ };
95
+
96
+ /** Kinds whose `_def.defaultValue` supplies the field's default. */
97
+ const DEFAULT_KINDS = new Set(['default', 'prefault']);
98
+
99
+ // --- accessors that read the same on both majors -----------------------------
100
+ // Each tries the public getter first (both majors expose it), then the
101
+ // major-specific `_def` keys, guarding shape so v4's `_def.type: 'array'`
102
+ // string is never mistaken for a schema.
103
+
104
+ function description(node: ZodNode): string | undefined {
105
+ const own = node.description;
106
+ if (typeof own === 'string') return own;
107
+ const fromDef = node._def?.description;
108
+ return typeof fromDef === 'string' ? fromDef : undefined;
109
+ }
110
+
111
+ function allStrings(value: unknown): string[] | null {
112
+ return Array.isArray(value) && value.every((v) => typeof v === 'string')
113
+ ? (value as string[])
114
+ : null;
115
+ }
116
+
117
+ function enumOptions(node: ZodNode): string[] | null {
118
+ const fromGetter = allStrings(node.options);
119
+ if (fromGetter) return fromGetter;
120
+ const def = node._def ?? {};
121
+ const fromValues = allStrings(def.values); // v3
122
+ if (fromValues) return fromValues;
123
+ const entries = def.entries; // v4 — an object map, not an array
124
+ if (entries && typeof entries === 'object') return allStrings(Object.values(entries));
125
+ return null;
126
+ }
127
+
128
+ function arrayElement(node: ZodNode): ZodNode | null {
129
+ const def = node._def ?? {};
130
+ for (const candidate of [node.element, def.element, def.type]) {
131
+ if (candidate && typeof candidate === 'object') return candidate as ZodNode;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ function literalValue(node: ZodNode): unknown {
137
+ const def = node._def ?? {};
138
+ if (def.value !== undefined) return def.value; // v3
139
+ return Array.isArray(def.values) ? def.values[0] : undefined; // v4
140
+ }
141
+
142
+ function shape(node: ZodNode): Record<string, ZodNode> | null {
143
+ try {
144
+ const raw = node._def?.shape;
145
+ const resolved = typeof raw === 'function' ? (raw as () => unknown)() : raw;
146
+ return resolved && typeof resolved === 'object'
147
+ ? (resolved as Record<string, ZodNode>)
148
+ : null;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ /** Peel wrappers using this major's table. Bounded loop: wrapper chains are
155
+ * short, and 20 guards against a cyclic definition. */
156
+ function makeUnwrap(
157
+ wrappers: Record<string, Wrapper>,
158
+ kind: (node: ZodNode) => string | null,
159
+ ): (node: ZodNode) => Unwrapped {
160
+ return (schema) => {
161
+ let inner = schema;
162
+ let required = true;
163
+ let pinned = false;
164
+ let defaultValue: unknown;
165
+ for (let i = 0; i < 20; i++) {
166
+ const k = kind(inner);
167
+ const wrapper = k ? wrappers[k] : undefined;
168
+ if (!wrapper) break;
169
+ const def = inner._def ?? {};
170
+ if (wrapper.pins) {
171
+ required = true;
172
+ pinned = true;
173
+ } else if (wrapper.optional && !pinned) {
174
+ required = false;
175
+ }
176
+ if (k && DEFAULT_KINDS.has(k)) {
177
+ const declared = def.defaultValue;
178
+ try {
179
+ // v3 stores a thunk; v4 stores the value itself.
180
+ defaultValue = typeof declared === 'function'
181
+ ? (declared as () => unknown)()
182
+ : declared;
183
+ } catch {
184
+ /* a throwing default must never break introspection */
185
+ }
186
+ }
187
+ const next = def[wrapper.key];
188
+ if (!next || typeof next !== 'object') break;
189
+ inner = next as ZodNode;
190
+ }
191
+ return { inner, required, defaultValue };
192
+ };
193
+ }
194
+
195
+ function kindV3(node: ZodNode): string | null {
196
+ const typeName = node._def?.typeName;
197
+ // 'ZodString' → 'string', matching v4's own naming so callers switch once.
198
+ return typeof typeName === 'string' ? typeName.replace(/^Zod/, '').toLowerCase() : null;
199
+ }
200
+
201
+ function kindV4(node: ZodNode): string | null {
202
+ const type = node._def?.type;
203
+ return typeof type === 'string' ? type : null;
204
+ }
205
+
206
+ const V3: ZodAdapter = {
207
+ major: 3,
208
+ kind: kindV3,
209
+ unwrap: makeUnwrap(V3_WRAPPERS, kindV3),
210
+ description,
211
+ enumOptions,
212
+ arrayElement,
213
+ literalValue,
214
+ shape,
215
+ };
216
+
217
+ const V4: ZodAdapter = {
218
+ major: 4,
219
+ kind: kindV4,
220
+ unwrap: makeUnwrap(V4_WRAPPERS, kindV4),
221
+ description,
222
+ enumOptions,
223
+ arrayElement,
224
+ literalValue,
225
+ shape,
226
+ };
227
+
228
+ /**
229
+ * Pick the adapter for whatever zod instance produced this schema, or null when
230
+ * it isn't a recognizable zod schema at all (a function schema the provider
231
+ * couldn't call, a future major, a plain object).
232
+ */
233
+ export function adapterFor(schema: unknown): ZodAdapter | null {
234
+ const def = (schema as ZodNode | null | undefined)?._def;
235
+ if (!def) return null;
236
+ if (typeof def.typeName === 'string') return V3;
237
+ if (typeof def.type === 'string') return V4;
238
+ return null;
239
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Conversions between the universes an asset path lives in — the two the entry
3
+ * drawer straddles, plus the URL form any of them takes once it is written into
4
+ * markup (`webPathToUrl`).
5
+ *
6
+ * Two path universes meet in the entry drawer. A plain `<img src>` in a
7
+ * `.astro` file needs a **web-servable, root-relative** path (`/images/hero.png`)
8
+ * that still resolves in the built site. A field backed by Astro's `image()`
9
+ * schema helper needs an **entry-file-relative** path (`../../assets/hero.png`)
10
+ * that Vite can import at build time. This module is the only place that
11
+ * converts between them.
12
+ *
13
+ * Runtime-shared (client + server), like `slug.ts` — so it is pure posix string
14
+ * math with no `node:path` import and no DOM. Every function returns `null`
15
+ * rather than throwing, and refuses anything that would escape the project root.
16
+ */
17
+
18
+ /** Only assets under `src/` can back an `image()` field: Astro imports them
19
+ * through Vite, and files in `public/` are copied verbatim, never importable. */
20
+ const IMPORTABLE_PREFIX = 'src';
21
+
22
+ /** Split a path into segments, tolerating Windows separators and dropping the
23
+ * no-op `.` and empty parts. */
24
+ function segmentsOf(path: string): string[] {
25
+ return path.replace(/\\/g, '/').split('/').filter((s) => s !== '' && s !== '.');
26
+ }
27
+
28
+ /** Resolve `..` segments, or null when they climb past the root. */
29
+ function normalize(parts: string[]): string[] | null {
30
+ const out: string[] = [];
31
+ for (const part of parts) {
32
+ if (part === '..') {
33
+ if (out.length === 0) return null; // escapes the project root
34
+ out.pop();
35
+ continue;
36
+ }
37
+ out.push(part);
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** The directory segments of a repo-relative file path. */
43
+ function dirSegments(file: string): string[] {
44
+ const parts = segmentsOf(file);
45
+ return parts.slice(0, -1);
46
+ }
47
+
48
+ /**
49
+ * An entry-relative asset value → the path the dev server serves it at.
50
+ *
51
+ * `('src/content/blog/post.md', '../../assets/blog/hero.png')`
52
+ * → `'/src/assets/blog/hero.png'`
53
+ *
54
+ * A value that is already root-relative is returned unchanged: it is the wrong
55
+ * shape for the field, but previewing what it actually points at is more useful
56
+ * than showing nothing. Returns null for an empty value or one that climbs out
57
+ * of the project root.
58
+ */
59
+ export function entryRelativeToWeb(entryFile: string, value: string): string | null {
60
+ const trimmed = value.trim();
61
+ if (!trimmed) return null;
62
+ if (trimmed.startsWith('/')) return trimmed;
63
+ // A bare URL or data: value is not a file path at all — hand it back for the
64
+ // preview to attempt, and let the caller decide it isn't a valid field value.
65
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return trimmed;
66
+ const resolved = normalize([...dirSegments(entryFile), ...segmentsOf(trimmed)]);
67
+ return resolved && resolved.length ? '/' + resolved.join('/') : null;
68
+ }
69
+
70
+ /**
71
+ * A served asset path → the entry-relative value to store in frontmatter.
72
+ *
73
+ * `('src/content/blog/post.md', '/src/assets/blog/hero.png')`
74
+ * → `'../../assets/blog/hero.png'`
75
+ *
76
+ * Returns null when the asset can't back an `image()` field — anything outside
77
+ * `src/`, which covers every `public/` asset, since Astro cannot import those.
78
+ */
79
+ export function webToEntryRelative(entryFile: string, webPath: string): string | null {
80
+ if (!webPath.startsWith('/')) return null;
81
+ const target = normalize(segmentsOf(webPath));
82
+ if (!target || target[0] !== IMPORTABLE_PREFIX) return null;
83
+ const from = normalize(dirSegments(entryFile));
84
+ if (!from) return null;
85
+
86
+ let common = 0;
87
+ while (common < from.length && common < target.length && from[common] === target[common]) {
88
+ common++;
89
+ }
90
+ const up = new Array(from.length - common).fill('..');
91
+ const down = target.slice(common);
92
+ if (down.length === 0) return null; // the asset *is* the directory
93
+ // A bare `hero.png` reads as a bare module specifier to Vite; `./hero.png`
94
+ // is unambiguously a sibling file.
95
+ return up.length ? [...up, ...down].join('/') : './' + down.join('/');
96
+ }
97
+
98
+ /**
99
+ * The root-relative directory an `image()` field's current value lives in, for
100
+ * uploads that should land beside their siblings rather than in a shared root.
101
+ *
102
+ * `('src/content/blog/post.md', '../../assets/blog/hero.png')` → `'src/assets/blog'`
103
+ *
104
+ * Returns null when there is no value, or when it doesn't resolve to an
105
+ * importable `src/` asset — the caller then falls back to its configured dir.
106
+ */
107
+ export function entryAssetDir(entryFile: string, value: string): string | null {
108
+ const web = entryRelativeToWeb(entryFile, value);
109
+ if (!web || !web.startsWith('/')) return null;
110
+ const parts = normalize(segmentsOf(web));
111
+ if (!parts || parts[0] !== IMPORTABLE_PREFIX) return null;
112
+ const dir = parts.slice(0, -1);
113
+ return dir.length ? dir.join('/') : null;
114
+ }
115
+
116
+ /**
117
+ * A web path as it must be written into markup: every segment
118
+ * percent-encoded.
119
+ *
120
+ * A filesystem name may hold characters a URL path may not — a space above
121
+ * all — and the paths this module and `toWebPath` deal in are descriptions of
122
+ * files, so they carry those characters raw. A browser forgives a raw space
123
+ * when it fetches one, which is why this is easy to miss, but `src="/a b.png"`
124
+ * is not a valid URI and is not how anyone writes that path by hand.
125
+ *
126
+ * Only for a path that is **not yet** encoded — one this tool composed from a
127
+ * filename. Running it over a value a person typed would turn their `%20` into
128
+ * `%2520`, so a hand-edited field keeps what it was given.
129
+ */
130
+ export function webPathToUrl(webPath: string): string {
131
+ return webPath.split('/').map(encodeURIComponent).join('/');
132
+ }