@velastack/cms 0.6.3 → 0.6.5

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 (48) hide show
  1. package/README.md +96 -4
  2. package/dist/components/admin-bar/admin-bar-internal.svelte +18 -12
  3. package/dist/components/admin-bar/admin-bar.css +1 -1
  4. package/dist/components/admin-bar/admin-bar.svelte +1 -1
  5. package/dist/components/admin-bar/field-input.svelte +1 -1
  6. package/dist/components/admin-bar/history-panel.svelte +1 -1
  7. package/dist/components/admin-bar/structured-editor-popover.svelte +21 -3
  8. package/dist/components/admin-bar/ui/badge/badge.svelte.d.ts +1 -1
  9. package/dist/components/cms/cms-form-controls.svelte +23 -0
  10. package/dist/components/cms/cms-form-controls.svelte.d.ts +13 -0
  11. package/dist/components/cms/cms-form-fields.svelte +68 -0
  12. package/dist/components/cms/cms-form-fields.svelte.d.ts +21 -0
  13. package/dist/components/cms/cms-form.svelte +99 -0
  14. package/dist/components/cms/cms-form.svelte.d.ts +35 -0
  15. package/dist/components/cms/cms-whatsapp-form-editable.svelte +43 -0
  16. package/dist/components/cms/cms-whatsapp-form-editable.svelte.d.ts +17 -0
  17. package/dist/components/cms/cms-whatsapp-form.svelte +167 -0
  18. package/dist/components/cms/cms-whatsapp-form.svelte.d.ts +43 -0
  19. package/dist/components/cms/index.d.ts +5 -0
  20. package/dist/components/cms/index.js +3 -0
  21. package/dist/components/cms/use-form-target.svelte.d.ts +18 -0
  22. package/dist/components/cms/use-form-target.svelte.js +68 -0
  23. package/dist/core/forms/target.d.ts +3 -0
  24. package/dist/core/forms/target.js +16 -0
  25. package/dist/core/forms/whatsapp.d.ts +18 -0
  26. package/dist/core/forms/whatsapp.js +29 -0
  27. package/dist/core/shapes/form-fields.d.ts +20 -0
  28. package/dist/core/shapes/form-fields.js +117 -0
  29. package/dist/core/shapes/index.d.ts +4 -0
  30. package/dist/core/shapes/index.js +2 -0
  31. package/dist/core/shapes/whatsapp.d.ts +8 -0
  32. package/dist/core/shapes/whatsapp.js +28 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +6 -0
  35. package/dist/server/load-cms.js +10 -7
  36. package/dist/server/mock-adapter.js +4 -0
  37. package/package.json +6 -1
  38. package/plugin/dist/build.d.ts +34 -2
  39. package/plugin/dist/build.js +58 -2
  40. package/plugin/dist/build.js.map +1 -1
  41. package/plugin/dist/index.js +66 -20
  42. package/plugin/dist/index.js.map +1 -1
  43. package/plugin/dist/manifest.d.ts +22 -0
  44. package/plugin/dist/manifest.js +35 -4
  45. package/plugin/dist/manifest.js.map +1 -1
  46. package/plugin/dist/parse-svelte.d.ts +6 -0
  47. package/plugin/dist/parse-svelte.js +5 -0
  48. package/plugin/dist/parse-svelte.js.map +1 -1
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Building the `wa.me` link a `CmsWhatsAppForm` opens: the editor's format
3
+ * string with the visitor's answers substituted, sent to the editor's number.
4
+ */
5
+ const PLACEHOLDER = /\{([^{}\s]+)\}/g;
6
+ /**
7
+ * Substitute `{field}` placeholders with the submitted values. A field sent
8
+ * more than once (checkboxes, multi-selects) joins with commas; a placeholder
9
+ * with no matching field, and file uploads, become empty.
10
+ */
11
+ export const fillMessage = (template, data) => template.replace(PLACEHOLDER, (_, name) => data
12
+ .getAll(name)
13
+ .filter((value) => typeof value === 'string' && value !== '')
14
+ .join(', '));
15
+ /**
16
+ * The digits `wa.me` wants: country code and number, no `+`, spaces or
17
+ * dashes. A leading `00` international prefix is dropped, since no country
18
+ * code starts with 0.
19
+ */
20
+ export const whatsappNumber = (raw) => raw.replace(/\D/g, '').replace(/^00/, '');
21
+ /** `https://wa.me/<number>?text=<message>`, or `null` when no number is set. */
22
+ export const whatsappHref = (number, message = '') => {
23
+ const digits = whatsappNumber(number);
24
+ if (!digits)
25
+ return null;
26
+ return message
27
+ ? `https://wa.me/${digits}?text=${encodeURIComponent(message)}`
28
+ : `https://wa.me/${digits}`;
29
+ };
@@ -0,0 +1,20 @@
1
+ import { type ListValue } from './form.js';
2
+ export declare const FORM_FIELD_TYPES: readonly ["text", "email", "tel", "number", "date", "textarea", "select", "checkbox"];
3
+ export type CmsFormFieldType = (typeof FORM_FIELD_TYPES)[number];
4
+ export type CmsFormFieldItem = {
5
+ id: string;
6
+ /** The submitted field name. */
7
+ name: string;
8
+ label: string;
9
+ type: CmsFormFieldType;
10
+ required: boolean;
11
+ placeholder: string;
12
+ /** `select`: the choices, in order. */
13
+ options: string[];
14
+ };
15
+ export type CmsFormFieldsValue = ListValue<CmsFormFieldItem>;
16
+ /** `Preferred date` → `preferred_date`; a label with no latin letters → `field`. */
17
+ export declare const fieldNameFromLabel: (label: string) => string;
18
+ export declare const cmsFormFields: import("./form.js").FormShape<CmsFormFieldsValue>;
19
+ /** What a template renders: every field, in order. */
20
+ export declare const formFieldsView: (value: CmsFormFieldsValue) => CmsFormFieldItem[];
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `CmsFormFields`: the inputs of a form, as content an editor can add to,
3
+ * remove from, reorder and retype.
4
+ *
5
+ * A field's `name` is the key its answer is stored under, so it must not
6
+ * follow later label edits or a form's submissions would split across keys.
7
+ * Fields from the template's fallback carry their own names. A field an
8
+ * editor adds has none until its label is first saved; reads derive one from
9
+ * the label, and the next save through the popover writes it down, after
10
+ * which renaming the label leaves it alone. The editor never shows `name`.
11
+ */
12
+ import { asBoolean, asEnum, asItems, asString, asStrings, isPlainObject } from '../structured.js';
13
+ import { defineForm } from './form.js';
14
+ export const FORM_FIELD_TYPES = [
15
+ 'text',
16
+ 'email',
17
+ 'tel',
18
+ 'number',
19
+ 'date',
20
+ 'textarea',
21
+ 'select',
22
+ 'checkbox'
23
+ ];
24
+ /** `Preferred date` → `preferred_date`; a label with no latin letters → `field`. */
25
+ export const fieldNameFromLabel = (label) => label
26
+ .normalize('NFKD')
27
+ .replace(/[̀-ͯ]/g, '')
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, '_')
30
+ .replace(/^_+|_+$/g, '') || 'field';
31
+ /**
32
+ * Give every field a distinct name. Stored names are kept and claimed
33
+ * first, so a newly added field can never take one an existing field's
34
+ * submissions already use; derived names get `_2`, `_3`, … on a clash.
35
+ */
36
+ const assignNames = (items) => {
37
+ const taken = new Set();
38
+ const named = items.map((item) => {
39
+ if (!item.stored || taken.has(item.stored))
40
+ return { ...item, name: '' };
41
+ taken.add(item.stored);
42
+ return { ...item, name: item.stored };
43
+ });
44
+ return named.map(({ stored: _stored, ...item }) => {
45
+ if (item.name)
46
+ return item;
47
+ const base = fieldNameFromLabel(item.label);
48
+ let name = base;
49
+ for (let n = 2; taken.has(name); n++)
50
+ name = `${base}_${n}`;
51
+ taken.add(name);
52
+ return { ...item, name };
53
+ });
54
+ };
55
+ export const cmsFormFields = defineForm({
56
+ component: 'CmsFormFields',
57
+ label: 'Form fields',
58
+ version: 1,
59
+ translatable: [],
60
+ items: { key: 'items', translatable: ['label', 'placeholder', 'options'] },
61
+ normalize: (raw) => {
62
+ const items = Array.isArray(raw) ? raw : isPlainObject(raw) ? raw.items : [];
63
+ return {
64
+ v: 1,
65
+ items: assignNames(asItems(items, (r) => ({
66
+ stored: asString(r.name).trim(),
67
+ name: '',
68
+ label: asString(r.label),
69
+ type: asEnum(r.type, FORM_FIELD_TYPES, 'text'),
70
+ required: asBoolean(r.required),
71
+ placeholder: asString(r.placeholder),
72
+ options: asStrings(r.options)
73
+ })))
74
+ };
75
+ },
76
+ empty: () => ({ v: 1, items: [] }),
77
+ fields: [
78
+ {
79
+ key: 'items',
80
+ label: 'Fields',
81
+ type: 'list',
82
+ itemLabel: 'field',
83
+ titleKey: 'label',
84
+ blank: () => ({ label: '', type: 'text', required: false, placeholder: '', options: [] }),
85
+ fields: [
86
+ { key: 'label', label: 'Label', type: 'text' },
87
+ {
88
+ key: 'type',
89
+ label: 'Type',
90
+ type: 'enum',
91
+ values: FORM_FIELD_TYPES,
92
+ names: {
93
+ text: 'Short text',
94
+ email: 'Email',
95
+ tel: 'Phone',
96
+ number: 'Number',
97
+ date: 'Date',
98
+ textarea: 'Long text',
99
+ select: 'Dropdown',
100
+ checkbox: 'Checkbox'
101
+ },
102
+ half: true
103
+ },
104
+ { key: 'required', label: 'Required', type: 'boolean' },
105
+ { key: 'placeholder', label: 'Placeholder', type: 'text' },
106
+ {
107
+ key: 'options',
108
+ label: 'Dropdown choices',
109
+ type: 'strings',
110
+ placeholder: 'One per line'
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+ });
116
+ /** What a template renders: every field, in order. */
117
+ export const formFieldsView = (value) => value.items;
@@ -27,5 +27,9 @@ export { cmsPricing, pricingView, formatPrice } from './pricing.js';
27
27
  export type { CmsPricingValue, PricingFeature, PricingTier, PricingTierView } from './pricing.js';
28
28
  export { cmsFaq, faqView, toFaqPage } from './faq.js';
29
29
  export type { CmsFaqValue, FaqItem } from './faq.js';
30
+ export { cmsFormFields, formFieldsView, fieldNameFromLabel, FORM_FIELD_TYPES } from './form-fields.js';
31
+ export type { CmsFormFieldsValue, CmsFormFieldItem, CmsFormFieldType } from './form-fields.js';
32
+ export { cmsWhatsApp } from './whatsapp.js';
33
+ export type { CmsWhatsAppValue } from './whatsapp.js';
30
34
  export { cmsStats, cmsSteps, cmsTimeline, cmsGallery, cmsLogos, cmsSchedule, logosView, scheduleByDay } from './presets.js';
31
35
  export type { CmsStatsValue, StatItem, CmsStepsValue, StepItem, CmsTimelineValue, TimelineItem, CmsGalleryValue, GalleryItem, CmsLogosValue, LogoItem, LogoItemView, CmsScheduleValue, ScheduleItem } from './presets.js';
@@ -10,4 +10,6 @@ export { cmsTestimonials, testimonialsView, toReview } from './testimonials.js';
10
10
  export { cmsCollection, collectionView, collectionTags, formatCollectionPrice, toItemList } from './collection.js';
11
11
  export { cmsPricing, pricingView, formatPrice } from './pricing.js';
12
12
  export { cmsFaq, faqView, toFaqPage } from './faq.js';
13
+ export { cmsFormFields, formFieldsView, fieldNameFromLabel, FORM_FIELD_TYPES } from './form-fields.js';
14
+ export { cmsWhatsApp } from './whatsapp.js';
13
15
  export { cmsStats, cmsSteps, cmsTimeline, cmsGallery, cmsLogos, cmsSchedule, logosView, scheduleByDay } from './presets.js';
@@ -0,0 +1,8 @@
1
+ export type CmsWhatsAppValue = {
2
+ v: 1;
3
+ /** International format; anything but digits is dropped when linking. */
4
+ number: string;
5
+ /** The message, with `{field}` placeholders for the visitor's answers. */
6
+ message: string;
7
+ };
8
+ export declare const cmsWhatsApp: import("./form.js").FormShape<CmsWhatsAppValue>;
@@ -0,0 +1,28 @@
1
+ /** `CmsWhatsAppForm`: the number a form's answers go to, and how they read. */
2
+ import { asString, isPlainObject } from '../structured.js';
3
+ import { defineForm } from './form.js';
4
+ export const cmsWhatsApp = defineForm({
5
+ component: 'CmsWhatsAppForm',
6
+ label: 'WhatsApp',
7
+ version: 1,
8
+ translatable: ['message'],
9
+ normalize: (raw) => {
10
+ const r = isPlainObject(raw) ? raw : {};
11
+ return { v: 1, number: asString(r.number), message: asString(r.message) };
12
+ },
13
+ empty: () => ({ v: 1, number: '', message: '' }),
14
+ fields: [
15
+ {
16
+ key: 'number',
17
+ label: 'WhatsApp number',
18
+ type: 'text',
19
+ placeholder: '+1 555 010 0199'
20
+ },
21
+ {
22
+ key: 'message',
23
+ label: 'Message',
24
+ type: 'long-string',
25
+ placeholder: "Hi, I'm {name}…"
26
+ }
27
+ ]
28
+ });
package/dist/index.d.ts CHANGED
@@ -31,6 +31,11 @@ export { default as CmsTimeline } from './components/cms/cms-timeline.svelte';
31
31
  export { default as CmsGallery } from './components/cms/cms-gallery.svelte';
32
32
  export { default as CmsLogos } from './components/cms/cms-logos.svelte';
33
33
  export { default as CmsSchedule } from './components/cms/cms-schedule.svelte';
34
+ export { default as CmsForm } from './components/cms/cms-form.svelte';
35
+ export type { CmsFormState, CmsFormStatus } from './components/cms/cms-form.svelte';
36
+ export { default as CmsFormFields } from './components/cms/cms-form-fields.svelte';
37
+ export { default as CmsWhatsAppForm } from './components/cms/cms-whatsapp-form.svelte';
38
+ export type { CmsWhatsAppFormState } from './components/cms/cms-whatsapp-form.svelte';
34
39
  export * from './core/shapes/index.js';
35
40
  export { default as AdminBar } from './components/admin-bar/admin-bar.svelte';
36
41
  export { useCmsField, resolveScopeRef, ROOT_SCOPE_ID } from './components/cms/use-cms-field.svelte.js';
package/dist/index.js CHANGED
@@ -33,6 +33,12 @@ export { default as CmsTimeline } from './components/cms/cms-timeline.svelte';
33
33
  export { default as CmsGallery } from './components/cms/cms-gallery.svelte';
34
34
  export { default as CmsLogos } from './components/cms/cms-logos.svelte';
35
35
  export { default as CmsSchedule } from './components/cms/cms-schedule.svelte';
36
+ // Forms: post to a hosted form by `key` or the template's own `action`.
37
+ // The inputs are the template's children, or a `CmsFormFields` block when
38
+ // editors should control them.
39
+ export { default as CmsForm } from './components/cms/cms-form.svelte';
40
+ export { default as CmsFormFields } from './components/cms/cms-form-fields.svelte';
41
+ export { default as CmsWhatsAppForm } from './components/cms/cms-whatsapp-form.svelte';
36
42
  export * from './core/shapes/index.js';
37
43
  // Admin bar (sync wrapper; admin-bar-internal + seo-panel are async chunks)
38
44
  export { default as AdminBar } from './components/admin-bar/admin-bar.svelte';
@@ -169,12 +169,15 @@ export const resolveCmsPayload = async (args) => {
169
169
  };
170
170
  };
171
171
  /**
172
- * A query parameter of the request, or null where the page cannot have one:
173
- * while building, and on a prerendered page, where SvelteKit (3.0 in dev too,
174
- * via `prerender_default`) replaces `url.search`/`url.searchParams` with a
175
- * getter that throws `url_search_unavailable_prerender`. A prerendered page
176
- * cannot vary by preview or version key, so null is the right answer, not an
177
- * error that takes the whole page down in `vite dev`.
172
+ * A query parameter of the request, or null while building.
173
+ *
174
+ * On a page with prerendering enabled, SvelteKit 3 replaces `url.search` and
175
+ * `url.searchParams` with a getter that throws `url_search_unavailable_prerender`,
176
+ * in `vite dev` as well as during a build. That guards prerendered output,
177
+ * which `building` already covers: nothing written to disk varies by preview
178
+ * or version key. Outside a build the page is rendered on demand, so the key
179
+ * is read from the raw request URL instead. Without it a page that exists
180
+ * only in the open release (one just created in the admin bar) 404s in dev.
178
181
  */
179
182
  const searchParam = (event, key) => {
180
183
  if (building)
@@ -183,7 +186,7 @@ const searchParam = (event, key) => {
183
186
  return event.url.searchParams.get(key);
184
187
  }
185
188
  catch {
186
- return null;
189
+ return new URL(event.request.url).searchParams.get(key);
187
190
  }
188
191
  };
189
192
  /**
@@ -129,6 +129,10 @@ export const mockAdapter = (options = {}) => {
129
129
  }
130
130
  }
131
131
  if (e.tombstone) {
132
+ // Like apiAdapter, a published read drops `gone`: prerender can
133
+ // only fail on a 410, while a redirect becomes a redirect page.
134
+ if (e.tombstone.kind === 'gone' && !previewKey)
135
+ continue;
132
136
  out.push({
133
137
  params: { ...e.params },
134
138
  metadata: metaOf(e.published),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velastack/cms",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "A scope-aware CMS for SvelteKit, for static or dynamic sites.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -134,5 +134,10 @@
134
134
  "#lib": "./src/lib/index.js",
135
135
  "#lib/*": "./src/lib/*",
136
136
  "#locales/*": "./src/locales/*"
137
+ },
138
+ "overrides": {
139
+ "runed": {
140
+ "@sveltejs/kit": "$@sveltejs/kit"
141
+ }
137
142
  }
138
143
  }
@@ -1,7 +1,7 @@
1
- import { buildManifest, resolveScopeAttr, ROOT_SCOPE_ID, type BuildManifestOptions, type BuildManifestResult, type CmsManifest, type CmsManifestScope, type CmsManifestUsage } from './manifest.js';
1
+ import { buildManifest, resolveScopeAttr, ROOT_SCOPE_ID, type BuildManifestOptions, type BuildManifestResult, type CmsManifest, type CmsManifestScope, type CmsManifestUsage, type UsageSite } from './manifest.js';
2
2
  export { buildManifest, resolveScopeAttr, ROOT_SCOPE_ID };
3
3
  export type Tree = Record<string, unknown>;
4
- export type { BuildManifestOptions, BuildManifestResult, CmsManifest, CmsManifestScope, CmsManifestUsage };
4
+ export type { BuildManifestOptions, BuildManifestResult, CmsManifest, CmsManifestScope, CmsManifestUsage, UsageSite };
5
5
  export type CmsUsage = {
6
6
  /** Static `name=` literal, or `null` when absent or dynamic. */
7
7
  name: string | null;
@@ -42,6 +42,13 @@ export type FallbackResolver = (usage: CmsUsage) => unknown;
42
42
  export type InjectOptions = ScanOptions & {
43
43
  /** Replace an existing `fallback` / `initial` prop instead of skipping it. */
44
44
  overwrite?: boolean;
45
+ /**
46
+ * The file's CMS components, local → canonical, as {@link buildManifest}
47
+ * resolved them (`cmsLocalsByFile`). Without it, components are recognised
48
+ * by import string alone, which misses imports the walk resolves, such as a
49
+ * project's own barrel.
50
+ */
51
+ locals?: Map<string, string>;
45
52
  };
46
53
  export type InjectResult = {
47
54
  code: string;
@@ -72,3 +79,28 @@ export declare const inlineFallbacks: (source: string, resolve: Tree | FallbackR
72
79
  * tried leaf first, and the first one holding the value wins.
73
80
  */
74
81
  export declare const fallbackResolverFor: (result: Pick<BuildManifestResult, "scopesByFile">, file: string, content: Record<string, Tree>) => FallbackResolver;
82
+ /** A usage that renders nothing until an editor stores a value. */
83
+ export type MissingFallback = UsageSite;
84
+ /** A content value no usage reads. `path` is null when the whole scope has no route. */
85
+ export type StaleContent = {
86
+ locale: string;
87
+ scopeId: string;
88
+ path: string | null;
89
+ };
90
+ /**
91
+ * Check content manifests (`content/<locale>.json`, keyed by scope id)
92
+ * against the usages the manifest recorded.
93
+ *
94
+ * - `missing`: usages with no `fallback`/`initial`, no fallback markup, and no
95
+ * value in the default locale's content. `CmsBoolean` is exempt: without
96
+ * `initial` it reads `false`, a value.
97
+ * - `stale`: values nothing reads, in every locale: a scope no route has, or
98
+ * a path under a scope that no usage's `name` covers. A structured value is
99
+ * read as a whole, and a stale branch is reported once at its top. Page
100
+ * `metadata` (the SEO panel's, with schemas a route may extend) and `site`
101
+ * are not checked.
102
+ */
103
+ export declare const checkContent: (result: Pick<BuildManifestResult, "manifest" | "usageSites">, content: Record<string, Record<string, Tree> | null>, defaultLocale: string) => {
104
+ missing: MissingFallback[];
105
+ stale: StaleContent[];
106
+ };
@@ -7,7 +7,8 @@
7
7
  * into those usages as `fallback` props ({@link injectFallbacks}), and which
8
8
  * scope a file's values live in ({@link fallbackResolverFor}, on top of
9
9
  * {@link buildManifest}). Keeping them here means no consumer carries a Svelte
10
- * parser of its own.
10
+ * parser of its own. {@link checkContent} holds a site to the manifest: every
11
+ * usage has a fallback, and no content value outlives the usage that read it.
11
12
  */
12
13
  import { basename } from 'node:path';
13
14
  import { parseSvelteSource } from './parse-svelte.js';
@@ -101,7 +102,7 @@ const FALLBACK_PROP = { CmsBoolean: 'initial' };
101
102
  */
102
103
  export const injectFallbacks = (source, resolve, options = {}) => {
103
104
  const parsed = parseSvelteSource(source, options.filename);
104
- const locals = classifyImports(parsed.imports, options.components ?? []);
105
+ const locals = options.locals ?? classifyImports(parsed.imports, options.components ?? []);
105
106
  const resolver = typeof resolve === 'function' ? resolve : (u) => (u.name ? get(resolve, u.name) : undefined);
106
107
  const edits = [];
107
108
  const injected = [];
@@ -163,4 +164,59 @@ export const fallbackResolverFor = (result, file, content) => {
163
164
  return undefined;
164
165
  };
165
166
  };
167
+ /**
168
+ * Check content manifests (`content/<locale>.json`, keyed by scope id)
169
+ * against the usages the manifest recorded.
170
+ *
171
+ * - `missing`: usages with no `fallback`/`initial`, no fallback markup, and no
172
+ * value in the default locale's content. `CmsBoolean` is exempt: without
173
+ * `initial` it reads `false`, a value.
174
+ * - `stale`: values nothing reads, in every locale: a scope no route has, or
175
+ * a path under a scope that no usage's `name` covers. A structured value is
176
+ * read as a whole, and a stale branch is reported once at its top. Page
177
+ * `metadata` (the SEO panel's, with schemas a route may extend) and `site`
178
+ * are not checked.
179
+ */
180
+ export const checkContent = (result, content, defaultLocale) => {
181
+ const fallbacks = content[defaultLocale] ?? {};
182
+ const missing = result.usageSites.filter((u) => !u.fallback && u.component !== 'CmsBoolean' && get(fallbacks[u.scopeId], u.name) === undefined);
183
+ const names = new Map();
184
+ for (const route of Object.values(result.manifest.routes)) {
185
+ for (const scope of route.scopes) {
186
+ const entry = names.get(scope.scopeId) ?? { kind: scope.kind, names: new Set() };
187
+ for (const u of scope.usages)
188
+ entry.names.add(u.name);
189
+ names.set(scope.scopeId, entry);
190
+ }
191
+ }
192
+ const stale = [];
193
+ for (const [locale, tree] of Object.entries(content)) {
194
+ for (const [scopeId, value] of Object.entries(tree ?? {})) {
195
+ if (scopeId === 'site')
196
+ continue;
197
+ const scope = names.get(scopeId);
198
+ if (!scope) {
199
+ stale.push({ locale, scopeId, path: null });
200
+ continue;
201
+ }
202
+ const read = [...scope.names];
203
+ const walk = (node, path) => {
204
+ if (scope.kind === 'page' && path.split('.')[0] === 'metadata')
205
+ return;
206
+ if (read.some((n) => path === n || path.startsWith(n + '.')))
207
+ return;
208
+ const below = read.some((n) => n.startsWith(path + '.'));
209
+ if (!below || node === null || typeof node !== 'object') {
210
+ stale.push({ locale, scopeId, path });
211
+ return;
212
+ }
213
+ for (const [k, v] of Object.entries(node))
214
+ walk(v, `${path}.${k}`);
215
+ };
216
+ for (const [k, v] of Object.entries(value ?? {}))
217
+ walk(v, k);
218
+ }
219
+ }
220
+ return { missing, stale };
221
+ };
166
222
  //# sourceMappingURL=build.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
2
+ import { dirname, relative, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { transformWithOxc } from 'vite';
5
5
  import { setPageCmsModules } from './build-state.js';
6
6
  import { buildManifest, readPackageImports } from './manifest.js';
7
7
  import { deriveUploadsBase, downloadMedia, extractMediaUrls } from './media.js';
8
- import { fallbackResolverFor, injectFallbacks } from './build.js';
8
+ import { checkContent, fallbackResolverFor, injectFallbacks } from './build.js';
9
9
  import { INSTALL_IMPORT_SOURCE, PAGE_CMS_VIRTUAL_PREFIX, SELF_INSTALL_IMPORT_SOURCE, buildPagesModuleSource, hasGenerateEntriesImport, injectGenerateEntriesRouteId, injectScopeInstall, parsePageCmsIndex } from './transforms.js';
10
10
  const VIRTUAL_ID = 'virtual:vela-cms/manifest';
11
11
  const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_ID;
@@ -168,31 +168,63 @@ export const cms = (options = {}) => {
168
168
  let cachedSync = null;
169
169
  let velacmsRoots = null;
170
170
  let warned = false;
171
- // The default-locale content manifest, re-read when its mtime changes so
172
- // edits show up in dev without a restart.
171
+ // Content manifests, `content/<locale>.json`, re-read when their mtime
172
+ // changes so edits show up in dev without a restart. The default locale's
173
+ // values become fallbacks; every locale's are checked for staleness.
174
+ let contentDir = null;
173
175
  let contentPath = null;
174
- let contentCache = null;
175
- const readContent = () => {
176
- if (!contentPath)
177
- return null;
176
+ let defaultLocale = 'en';
177
+ const contentCache = new Map();
178
+ const readContentFile = (file) => {
178
179
  let mtime;
179
180
  try {
180
- mtime = statSync(contentPath).mtimeMs;
181
+ mtime = statSync(file).mtimeMs;
181
182
  }
182
183
  catch {
183
184
  return null;
184
185
  }
185
- if (contentCache && contentCache.mtime === mtime)
186
- return contentCache.tree;
186
+ const hit = contentCache.get(file);
187
+ if (hit && hit.mtime === mtime)
188
+ return hit.tree;
189
+ let tree = null;
187
190
  try {
188
- const tree = JSON.parse(readFileSync(contentPath, 'utf-8'));
189
- contentCache = { mtime, tree };
190
- return tree;
191
+ tree = JSON.parse(readFileSync(file, 'utf-8'));
191
192
  }
192
193
  catch (e) {
193
- config.logger.warn(`@velastack/cms: could not read ${contentPath}: ${String(e)}`);
194
- return null;
194
+ config.logger.warn(`@velastack/cms: could not read ${file}: ${String(e)}`);
195
+ }
196
+ contentCache.set(file, { mtime, tree });
197
+ return tree;
198
+ };
199
+ const readContent = () => (contentPath ? readContentFile(contentPath) : null);
200
+ // Usages without a fallback and content nothing reads, logged whenever the
201
+ // list changes: on the first manifest, after a route edit, after a content
202
+ // edit. Quiet while nothing changes.
203
+ let lastContentReport = '';
204
+ const reportContent = (result) => {
205
+ const content = {};
206
+ for (const locale of options.locales ?? [defaultLocale]) {
207
+ content[locale] = contentDir ? readContentFile(resolve(contentDir, `${locale}.json`)) : null;
208
+ }
209
+ const { missing, stale } = checkContent(result, content, defaultLocale);
210
+ const lines = [];
211
+ if (missing.length) {
212
+ lines.push(`@velastack/cms: ${missing.length} usage(s) have no fallback and render nothing until a value is stored:`, ...missing.map((m) => ` ${relative(viteRoot, m.file)}:${m.line} <${m.component} name="${m.name}"> (${m.scopeId})`));
195
213
  }
214
+ if (stale.length) {
215
+ const dir = relative(viteRoot, contentDir ?? 'content');
216
+ lines.push(`@velastack/cms: ${stale.length} content value(s) are not read by any component:`, ...stale.map((v) => v.path === null
217
+ ? ` ${dir}/${v.locale}.json "${v.scopeId}" (no route has this scope)`
218
+ : ` ${dir}/${v.locale}.json "${v.scopeId}" › "${v.path}"`));
219
+ }
220
+ const report = lines.join('\n');
221
+ if (report === lastContentReport)
222
+ return;
223
+ if (report)
224
+ config.logger.warn(report);
225
+ else if (lastContentReport)
226
+ config.logger.info('@velastack/cms: fallbacks and content are in sync');
227
+ lastContentReport = report;
196
228
  };
197
229
  const logWarnings = (result) => {
198
230
  if (warned)
@@ -237,6 +269,7 @@ export const cms = (options = {}) => {
237
269
  cachedSync = result;
238
270
  setPageCmsModules(result.pageCmsModules);
239
271
  logWarnings(result);
272
+ reportContent(result);
240
273
  return result;
241
274
  })();
242
275
  return cached;
@@ -266,9 +299,9 @@ export const cms = (options = {}) => {
266
299
  ? SELF_INSTALL_IMPORT_SOURCE
267
300
  : INSTALL_IMPORT_SOURCE;
268
301
  if (options.content !== false) {
269
- const dir = resolve(config.root, options.content ?? 'content');
270
- const defaultLocale = options.locales?.[0] ?? 'en';
271
- const file = resolve(dir, `${defaultLocale}.json`);
302
+ contentDir = resolve(config.root, options.content ?? 'content');
303
+ defaultLocale = options.locales?.[0] ?? 'en';
304
+ const file = resolve(contentDir, `${defaultLocale}.json`);
272
305
  contentPath = existsSync(file) ? file : null;
273
306
  }
274
307
  },
@@ -406,7 +439,8 @@ export const cms = (options = {}) => {
406
439
  if (content) {
407
440
  this.addWatchFile(contentPath);
408
441
  next = injectFallbacks(next, fallbackResolverFor(result, id, content), {
409
- filename: id
442
+ filename: id,
443
+ locals: result.cmsLocalsByFile.get(id)
410
444
  }).code;
411
445
  }
412
446
  const info = result.scopeByEntryPath.get(id);
@@ -430,6 +464,18 @@ export const cms = (options = {}) => {
430
464
  return { code: next, map: null };
431
465
  }
432
466
  },
467
+ configureServer(server) {
468
+ // Content files are not modules, so editing one (any locale) only
469
+ // re-runs the content check.
470
+ server.watcher.on('change', (file) => {
471
+ if (contentDir &&
472
+ cachedSync &&
473
+ file.startsWith(contentDir + '/') &&
474
+ file.endsWith('.json')) {
475
+ reportContent(cachedSync);
476
+ }
477
+ });
478
+ },
433
479
  handleHotUpdate({ file, server }) {
434
480
  const isSvelte = file.endsWith('.svelte');
435
481
  const isRouteScript = file.endsWith('+page.ts') || file.endsWith('+page.server.ts');