mechanica-shared 2.0.0-alpha.9 → 2.0.2

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/src/types.ts DELETED
@@ -1,334 +0,0 @@
1
- /**
2
- * Core data types shared between the plugin runtime, the editor, and the
3
- * (future) render service. This module is intentionally DOM- and
4
- * framework-free so the render side can import it cleanly.
5
- */
6
-
7
- import type { LocalesConfig } from './locale'
8
-
9
- /** Scope at which a {@link DataEntry} lives. */
10
- export type DataScope = 'site' | 'folder' | 'page'
11
-
12
- /** Metadata about the current page, exposed via `usePageData`. */
13
- export interface PageMeta {
14
- title?: string
15
- path?: string
16
- meta?: Record<string, unknown>
17
- /**
18
- * Set on paginated variants of a page (`/blog/2`, …): which chunk of its
19
- * paginated query this URL shows. Page 1 is the base path and carries none.
20
- */
21
- pagination?: { page: number; pageCount?: number }
22
- /** The locale this page renders in (multi-language sites). Omitted when i18n is off. */
23
- locale?: string
24
- /** Which locales this logical page has a translation for (the default included). */
25
- locales?: string[]
26
- /**
27
- * True when the requested locale has no translation file and the page is
28
- * rendering the default-locale content as a fallback (dev only — the editor
29
- * shows a "translate this page" banner). Never set at export.
30
- */
31
- localeFallback?: boolean
32
- }
33
-
34
- /**
35
- * Editor-facing metadata describing a *block type* — its schema and palette
36
- * presentation, not its placed content. Produced by the block compiler from a
37
- * `defineBlock(...)` descriptor.
38
- */
39
- export interface Block {
40
- id: string
41
- name: string
42
- /** Palette grouping (was `group` in v1). */
43
- category?: string
44
- /** Icon key shown in the block palette. */
45
- icon?: string
46
- description?: string
47
- /** Sort order within a category. */
48
- order?: number
49
- /** Hidden from the palette. */
50
- hidden?: boolean
51
- /** Available only in dev, stripped from production output. */
52
- devOnly?: boolean
53
- /**
54
- * Restrict the block to pages under these folders (folder paths relative to
55
- * `pages/`, e.g. `'docs'`; nested folders match by prefix). Omitted = offered
56
- * everywhere. Placed blocks always keep rendering — this only filters what
57
- * the palette offers.
58
- */
59
- folders?: string[]
60
- /**
61
- * Schema version of this block (defaults to 1). Bump it together with a
62
- * `migrate` function whenever a saved page's data needs reshaping (renamed
63
- * prop, changed type); placed blocks record the version they were written
64
- * with and migrate on load.
65
- */
66
- version?: number
67
- /**
68
- * Upgrade a placed block's data from an older schema version. Receives the
69
- * stored data and the version it was written with; mutate it in place or
70
- * return the replacement. Must handle every `from < version`.
71
- */
72
- migrate?: (data: Record<string, unknown>, from: number) => Record<string, unknown> | undefined | void
73
- /**
74
- * Example prop values used when the block renders outside a page — the
75
- * palette hover preview and the `/@mechanica/preview` route (`mechanica shot`).
76
- * Merged over schema defaults, so it only needs the props that matter visually.
77
- * A `$slots` key fills the block's slots with child blocks (see
78
- * `PreviewSlotEntry` in `mechanica`); unfilled slots preview as placeholders.
79
- */
80
- previewData?: Record<string, unknown>
81
- /** compact-json-schema describing the editable props. */
82
- props?: Record<string, unknown>
83
- /** Slot name → slot metadata (currently `true`). */
84
- slots?: Record<string, unknown>
85
- /**
86
- * Marks a block produced by the Block Composer (a {@link ComposedBlockDefinition}),
87
- * rather than a compiled SFC. The page editor uses it to offer Edit/Delete and
88
- * to group these under "Site blocks". Set by the composed-block factory.
89
- */
90
- composed?: boolean
91
- }
92
-
93
- /**
94
- * One entry in the Block Composer's components manifest (`src/composer.ts`,
95
- * declared with `defineComposerComponents`). It exposes a site's own design
96
- * system — a plain Vue SFC plus the compact-json-schema for its editable props —
97
- * as building material in the composer, without touching the block compiler.
98
- *
99
- * DOM-free here: `component` is typed `unknown`; the `mechanica` package narrows
100
- * it to a Vue `Component`. A manifest value may also be a bare string, which
101
- * re-exposes an existing compiled block (by id) in the composer palette.
102
- */
103
- export interface ComposerComponentDefinition {
104
- /** The Vue component rendered for this id (narrowed to `Component` in `mechanica`). */
105
- component?: unknown
106
- /** Display name in the composer's Components palette. */
107
- name: string
108
- /** VIcon name (or raw `<svg>`) for the palette card. */
109
- icon?: string
110
- /** compact-json-schema describing the editable props (same field registry as blocks). */
111
- props?: Record<string, unknown>
112
- /** Example prop values for the canvas / previews. */
113
- previewData?: Record<string, unknown>
114
- }
115
-
116
- /** A components-manifest entry: a definition, or a string id re-exposing a compiled block. */
117
- export type ComposerComponentEntry = ComposerComponentDefinition | string
118
-
119
- /** The built-in composer element kinds a design-system class can attach to. */
120
- export type ComposerElementKind = 'frame' | 'text' | 'image'
121
-
122
- /**
123
- * One design-system CSS class the site offers to the Block Composer (via
124
- * `defineComposer`'s `classes`). The manifest key IS the CSS class name — the
125
- * developer writes `.container` in their stylesheet, declares it here, and the
126
- * composer offers it as a **Style** on matching elements. See COMPOSER-MANIFEST.md.
127
- */
128
- export interface ComposerClassDefinition {
129
- /** Label shown in the Style select; defaults to the class name. */
130
- title?: string
131
- /** Which element kinds may pick this class. */
132
- on: ComposerElementKind | ComposerElementKind[]
133
- /**
134
- * Optional group. Classes sharing a group are mutually exclusive (one Style
135
- * select per group, single-pick); classes in different groups stack on the
136
- * element. Ungrouped classes share one implicit default group labelled "Style".
137
- * The developer guarantees groups are orthogonal, so precedence stays clear.
138
- */
139
- group?: string
140
- }
141
-
142
- /**
143
- * A `classes` entry: the full definition, or a shorthand — a bare kind (`'text'`)
144
- * or an array of kinds (`['frame', 'image']`), with the title defaulting to the
145
- * class name.
146
- */
147
- export type ComposerClassEntry = ComposerClassDefinition | ComposerElementKind | ComposerElementKind[]
148
-
149
- /** A class entry normalized for the composer (shorthands unfolded). */
150
- export interface ComposerClassDef {
151
- /** The CSS class name (the manifest key). */
152
- cls: string
153
- /** Label for the Style select. */
154
- title: string
155
- /** Element kinds that may pick it. */
156
- kinds: ComposerElementKind[]
157
- /** Group key (undefined = the default "Style" group). Same group = mutually exclusive. */
158
- group?: string
159
- }
160
-
161
- /**
162
- * The site's element-system breakpoints (max-widths in px). Two fixed tiers;
163
- * only the widths are configurable. Consumed by the generated element CSS and
164
- * the composer's device switcher. Defaults: `{ md: 1024, sm: 640 }`.
165
- */
166
- export interface ComposerBreakpoints {
167
- md: number
168
- sm: number
169
- }
170
-
171
- /**
172
- * The Block Composer manifest (`src/composer.ts`, declared with `defineComposer`).
173
- * A single home for the site's design system as seen by the composer: its
174
- * components, its CSS classes, and its breakpoints. All sections are optional —
175
- * a site that declares only `components` behaves exactly as before the split.
176
- */
177
- export interface ComposerManifest {
178
- /** The site's design-system components (was `defineComposerComponents`). */
179
- components?: Record<string, ComposerComponentEntry>
180
- /** Design-system CSS classes offered as element **Style** in the composer. */
181
- classes?: Record<string, ComposerClassEntry>
182
- /** Element-system breakpoints (max-widths, px). Numeric literals only. */
183
- breakpoints?: Partial<ComposerBreakpoints>
184
- }
185
-
186
- /**
187
- * A designer-assembled block ("composed block"): a named, parameterized
188
- * {@link ContentBlock} subtree saved as data. It is not compiled — at render
189
- * time it *expands* into its template through the normal block-render pipeline,
190
- * so it behaves like any other block (palette, settings form, export, shots).
191
- *
192
- * Authored in the Block Composer and persisted as `.mech/blocks/<id>.block.yml`
193
- * (codec in `mechanica-shared/block-format`); a developer can hand-write the
194
- * same file. See PLAN.md.
195
- */
196
- export interface ComposedBlockDefinition {
197
- /** Stable, kebab-case id; unique across compiled blocks and composed blocks. */
198
- id: string
199
- /** Display name shown in the palette. */
200
- name: string
201
- /** VIcon name shown on the palette card. */
202
- icon?: string
203
- /** Palette grouping; defaults to a "Site blocks" group in the editor. */
204
- category?: string
205
- /**
206
- * compact-json-schema for the props exposed out of the template (§ prop
207
- * bindings). Placed instances get an auto-generated settings form from this,
208
- * exactly like a compiled block's `props`.
209
- */
210
- props?: Record<string, unknown>
211
- /**
212
- * Example prop values for previews (palette hover, `/@mechanica/preview`,
213
- * `mechanica shot`). Captured from the canvas values at author time.
214
- */
215
- previewData?: Record<string, unknown>
216
- /**
217
- * The block body: element/block nodes. A value inside a node's `data` may be
218
- * a {@link PropBinding} (`{ $bind: 'propName' }`) that resolves from the
219
- * instance's props at render time (see `resolveComposedTemplate`).
220
- */
221
- template: ContentBlock[]
222
- }
223
-
224
- /**
225
- * A placeholder inside a composed template's data, replaced at render time by
226
- * the value of the named prop on the placed instance.
227
- */
228
- export interface PropBinding {
229
- $bind: string
230
- }
231
-
232
- /** A block placed in a page's content tree. */
233
- export interface ContentBlock {
234
- id: string
235
- blockId: string
236
- data: Record<string, unknown>
237
- /** Schema version the data was written with (absent = 1); see `Block.version`. */
238
- v?: number
239
- /** Either a single default-slot list or a map of named-slot lists. */
240
- children?: ContentBlock[] | Record<string, ContentBlock[]>
241
- }
242
-
243
- /** A shared data entry declared with `defineData`. */
244
- export interface DataEntry {
245
- id: string
246
- title?: string
247
- /** compact-json-schema describing the data shape. */
248
- props?: Record<string, unknown>
249
- /**
250
- * Translate this entry per locale (multi-language sites). Its site/folder
251
- * value is stored per locale (with fallback to the default locale) instead of
252
- * once, so shared strings (nav labels, footer) can differ by language. Ignored
253
- * when i18n is off. Page-scoped values are already per-locale (they live in
254
- * the translation file), so this only affects site/folder scope.
255
- */
256
- localized?: boolean
257
- }
258
-
259
- /**
260
- * The runtime state serialized onto the page (`window.state`) and read back by
261
- * the runtime to hydrate.
262
- */
263
- export interface State {
264
- content: ContentBlock[]
265
- /** Effective data: site < folder < page merged, with schema defaults filled. */
266
- data: Record<string, unknown>
267
- query?: Record<string, unknown>
268
- /**
269
- * Scope buckets for the editor, so it can edit each level and tell which
270
- * entries a page overrides. Only injected in dev (the runtime renders `data`).
271
- */
272
- siteData?: Record<string, unknown>
273
- folderData?: Record<string, unknown>
274
- pageData?: Record<string, unknown>
275
- /** The folder this page lives in (null at the root), so the editor can offer folder scope. */
276
- folder?: string | null
277
- /**
278
- * The default-locale page's content (dev only, on a non-default locale). Lets
279
- * the editor mark which fields a translation *overrides* vs inherits, and
280
- * offer a one-click reset to the inherited value. Absent when i18n is off or
281
- * on the default locale.
282
- */
283
- baseContent?: ContentBlock[]
284
- /** Base URL the page is served under (for routing/link resolution). */
285
- baseUrl?: string
286
- /** Current page metadata. */
287
- page?: PageMeta
288
- /**
289
- * The site's locale configuration (multi-language sites). Present in the
290
- * serialized state so the runtime can prefix internal links for the current
291
- * locale and build language switchers. Absent when i18n is off.
292
- */
293
- locales?: LocalesConfig
294
- /**
295
- * Dev only: this state belongs to a programmatically generated page
296
- * ({@link VirtualPage}) that has no `.page.md` file. The editor treats it as
297
- * read-only — it never queues a save against a nonexistent file.
298
- */
299
- generated?: boolean
300
- }
301
-
302
- /**
303
- * A page produced programmatically (the plugin's `generatePages` option) instead
304
- * of from a `.page.md` file — plain, serializable data. The plugin runs its
305
- * providers at build and bakes the output into the SSR bundle for the static
306
- * export; the same shape is what a future render backend consumes for on-demand
307
- * / webhook regeneration. One `VirtualPage` per (logical page × locale).
308
- */
309
- export interface VirtualPage {
310
- /** Logical (unprefixed) path, e.g. `/docs/api/math/mathf`. Localized per `locale` when served. */
311
- path: string
312
- /** The block tree to render — usually one template block parameterized by `data`. */
313
- content: ContentBlock[]
314
- /** Page-scoped data (head, layout, block props). Site data still merges under it. */
315
- data?: Record<string, unknown>
316
- /** Page meta: `title` (breadcrumb / SEO name), `noindex`, and `{{ page.meta.* }}` hints. */
317
- meta?: Record<string, unknown>
318
- /** The locale this page renders in. Omit for the default locale. */
319
- locale?: string
320
- /** Every locale this logical page exists in — for hreflang alternates + language switchers. */
321
- locales?: string[]
322
- /** Sitemap `<lastmod>` (ISO date). The provider supplies it; there is no file to stat. */
323
- lastmod?: string
324
- }
325
-
326
- /** A link target produced by the `smartLink` field type. */
327
- export interface PageLink {
328
- id: string
329
- url?: string
330
- query?: string
331
- title: string
332
- external?: boolean
333
- openNewTab?: boolean
334
- }
@@ -1,56 +0,0 @@
1
- import type { Block } from './types'
2
- import { walkTree, walkSchema } from './schema'
3
-
4
- /** A link on a page pointing at an internal path no page is exported for. */
5
- export interface LinkIssue {
6
- /** The page the broken link lives on. */
7
- page: string
8
- /** The internal URL with no matching page. */
9
- url: string
10
- }
11
-
12
- /** Normalize an internal URL for page-path comparison (drop query/hash/trailing slash). */
13
- export function normalizeInternalUrl(url: string): string {
14
- const bare = url.split(/[?#]/)[0]!
15
- const trimmed = bare.replace(/\/+$/, '')
16
- return trimmed === '' ? '/' : trimmed
17
- }
18
-
19
- /**
20
- * Collect the internal link targets on a page: every `smartLink`-formatted
21
- * field whose URL is site-relative (starts with `/`) and not marked external.
22
- */
23
- export function collectInternalLinks(content: unknown[], blocksMap: Map<string, Block>): string[] {
24
- const found: string[] = []
25
- walkTree(content as never, (block: any) => {
26
- const meta = blocksMap.get(block.blockId)
27
- if (!meta) return
28
- walkSchema(block.data, meta.props, (value: any, schema: any) => {
29
- if (schema?.format !== 'smartLink' || value == null) return
30
- const url = typeof value === 'string' ? value : value.url
31
- const external = typeof value === 'object' && value.external === true
32
- if (typeof url === 'string' && url.startsWith('/') && !external) found.push(url)
33
- })
34
- })
35
- return found
36
- }
37
-
38
- /**
39
- * Cross-check every page's internal `smartLink` targets against the set of
40
- * page paths that actually exist. Returns the dead links (empty = all good).
41
- * Meant for export/deploy time — a warning, not a hard failure, since a target
42
- * may be intentionally served by something else (redirects, external hosting).
43
- */
44
- export function validateLinks(
45
- pages: Array<{ path: string; content?: unknown[] }>,
46
- blocksMap: Map<string, Block>,
47
- ): LinkIssue[] {
48
- const known = new Set(pages.map((page) => normalizeInternalUrl(page.path)))
49
- const issues: LinkIssue[] = []
50
- for (const page of pages) {
51
- for (const url of collectInternalLinks(page.content ?? [], blocksMap)) {
52
- if (!known.has(normalizeInternalUrl(url))) issues.push({ page: page.path, url })
53
- }
54
- }
55
- return issues
56
- }