@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.
- package/README.md +96 -4
- package/dist/components/admin-bar/admin-bar-internal.svelte +18 -12
- package/dist/components/admin-bar/admin-bar.css +1 -1
- package/dist/components/admin-bar/admin-bar.svelte +1 -1
- package/dist/components/admin-bar/field-input.svelte +1 -1
- package/dist/components/admin-bar/history-panel.svelte +1 -1
- package/dist/components/admin-bar/structured-editor-popover.svelte +21 -3
- package/dist/components/admin-bar/ui/badge/badge.svelte.d.ts +1 -1
- package/dist/components/cms/cms-form-controls.svelte +23 -0
- package/dist/components/cms/cms-form-controls.svelte.d.ts +13 -0
- package/dist/components/cms/cms-form-fields.svelte +68 -0
- package/dist/components/cms/cms-form-fields.svelte.d.ts +21 -0
- package/dist/components/cms/cms-form.svelte +99 -0
- package/dist/components/cms/cms-form.svelte.d.ts +35 -0
- package/dist/components/cms/cms-whatsapp-form-editable.svelte +43 -0
- package/dist/components/cms/cms-whatsapp-form-editable.svelte.d.ts +17 -0
- package/dist/components/cms/cms-whatsapp-form.svelte +167 -0
- package/dist/components/cms/cms-whatsapp-form.svelte.d.ts +43 -0
- package/dist/components/cms/index.d.ts +5 -0
- package/dist/components/cms/index.js +3 -0
- package/dist/components/cms/use-form-target.svelte.d.ts +18 -0
- package/dist/components/cms/use-form-target.svelte.js +68 -0
- package/dist/core/forms/target.d.ts +3 -0
- package/dist/core/forms/target.js +16 -0
- package/dist/core/forms/whatsapp.d.ts +18 -0
- package/dist/core/forms/whatsapp.js +29 -0
- package/dist/core/shapes/form-fields.d.ts +20 -0
- package/dist/core/shapes/form-fields.js +117 -0
- package/dist/core/shapes/index.d.ts +4 -0
- package/dist/core/shapes/index.js +2 -0
- package/dist/core/shapes/whatsapp.d.ts +8 -0
- package/dist/core/shapes/whatsapp.js +28 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/server/load-cms.js +10 -7
- package/dist/server/mock-adapter.js +4 -0
- package/package.json +6 -1
- package/plugin/dist/build.d.ts +34 -2
- package/plugin/dist/build.js +58 -2
- package/plugin/dist/build.js.map +1 -1
- package/plugin/dist/index.js +66 -20
- package/plugin/dist/index.js.map +1 -1
- package/plugin/dist/manifest.d.ts +22 -0
- package/plugin/dist/manifest.js +35 -4
- package/plugin/dist/manifest.js.map +1 -1
- package/plugin/dist/parse-svelte.d.ts +6 -0
- package/plugin/dist/parse-svelte.js +5 -0
- 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';
|
package/dist/server/load-cms.js
CHANGED
|
@@ -169,12 +169,15 @@ export const resolveCmsPayload = async (args) => {
|
|
|
169
169
|
};
|
|
170
170
|
};
|
|
171
171
|
/**
|
|
172
|
-
* A query parameter of the request, or null
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* getter that throws `url_search_unavailable_prerender
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
|
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
|
+
"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
|
}
|
package/plugin/dist/build.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/plugin/dist/build.js
CHANGED
|
@@ -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
|
package/plugin/dist/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
package/plugin/dist/index.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
175
|
-
const
|
|
176
|
-
|
|
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(
|
|
181
|
+
mtime = statSync(file).mtimeMs;
|
|
181
182
|
}
|
|
182
183
|
catch {
|
|
183
184
|
return null;
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
const hit = contentCache.get(file);
|
|
187
|
+
if (hit && hit.mtime === mtime)
|
|
188
|
+
return hit.tree;
|
|
189
|
+
let tree = null;
|
|
187
190
|
try {
|
|
188
|
-
|
|
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 ${
|
|
194
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
const file = resolve(
|
|
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');
|