ampless 1.0.0-alpha.15 → 1.0.0-alpha.17

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/dist/index.d.ts CHANGED
@@ -69,7 +69,325 @@ declare function detectContentEvents(input: {
69
69
  newStatus?: 'draft' | 'published';
70
70
  }): ContentEventType[];
71
71
 
72
+ type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
73
+ /**
74
+ * Single entry in a `linkList` field. Stored as part of a JSON array
75
+ * under the field's storage key. `url` may be:
76
+ * - a relative path (`/about`)
77
+ * - an absolute URL (`https://example.com`)
78
+ * - a tag reference (`tag:guide`) — themes interpret this as
79
+ * "expand to a list of posts with this tag" rather than rendering
80
+ * a literal link.
81
+ */
82
+ interface LinkListItem {
83
+ label: string;
84
+ url: string;
85
+ }
86
+ /**
87
+ * A user-facing string in a manifest. Either a plain string (rendered
88
+ * as-is, regardless of locale) or a per-locale map (the renderer picks
89
+ * the active locale, falling back to `en`, then to any value).
90
+ *
91
+ * Themes that ship in a single language can keep these as plain
92
+ * strings. The default themes use the map form so the same manifest
93
+ * works for both built-in dictionaries.
94
+ */
95
+ type LocalizedString = string | Record<string, string>;
96
+ interface ThemeFieldBase {
97
+ /** Storage key. Persisted as `theme.{key}` in site settings. */
98
+ key: string;
99
+ label: LocalizedString;
100
+ description?: LocalizedString;
101
+ /** Optional UI grouping (e.g. 'Colors', 'Typography', 'Branding'). */
102
+ group?: LocalizedString;
103
+ /** Used when no override is set. Always a string for storage uniformity. */
104
+ default: string;
105
+ /**
106
+ * If set, the loader injects `${cssVar}: ${value}` into a `:root`
107
+ * style block on every public page, so CSS rules using
108
+ * `var(${cssVar})` pick up overrides at render time.
109
+ *
110
+ * Fields without `cssVar` (e.g. logo URL, header tagline) are exposed
111
+ * to template code via `loadThemeConfig()` instead.
112
+ */
113
+ cssVar?: string;
114
+ }
115
+ interface ThemeColorField extends ThemeFieldBase {
116
+ type: 'color';
117
+ }
118
+ interface ThemeTextField extends ThemeFieldBase {
119
+ type: 'text';
120
+ maxLength?: number;
121
+ }
122
+ interface ThemeSelectField extends ThemeFieldBase {
123
+ type: 'select';
124
+ options: ReadonlyArray<{
125
+ value: string;
126
+ label: LocalizedString;
127
+ }>;
128
+ }
129
+ interface ThemeImageField extends ThemeFieldBase {
130
+ type: 'image';
131
+ }
132
+ interface ThemeLengthField extends ThemeFieldBase {
133
+ type: 'length';
134
+ }
135
+ interface ThemeFontFamilyField extends ThemeFieldBase {
136
+ type: 'fontFamily';
137
+ options: ReadonlyArray<{
138
+ value: string;
139
+ label: LocalizedString;
140
+ }>;
141
+ }
142
+ /**
143
+ * A repeatable list of {label, url} entries — used for nav menus,
144
+ * footer link sets, sidebar groups, etc. Stored in KvStore as a JSON
145
+ * string so it fits the existing `string`-valued site-settings cache.
146
+ *
147
+ * `default` is declared as a plain array for ergonomics; the loader
148
+ * stringifies it on the fly so manifest authors don't have to call
149
+ * JSON.stringify by hand.
150
+ */
151
+ interface ThemeLinkListField extends Omit<ThemeFieldBase, 'default' | 'cssVar'> {
152
+ type: 'linkList';
153
+ default: ReadonlyArray<LinkListItem>;
154
+ /** Cap admin-supplied list length. Default 50. */
155
+ maxItems?: number;
156
+ }
157
+ type ThemeField = ThemeColorField | ThemeTextField | ThemeSelectField | ThemeImageField | ThemeLengthField | ThemeFontFamilyField | ThemeLinkListField;
158
+ interface ThemeManifest {
159
+ /** Theme directory name (`themes/<name>/`). */
160
+ name: string;
161
+ label: LocalizedString;
162
+ description?: LocalizedString;
163
+ fields: ReadonlyArray<ThemeField>;
164
+ }
165
+ declare function defineTheme(m: ThemeManifest): ThemeManifest;
166
+ /**
167
+ * Resolve a `LocalizedString` to a plain string for display.
168
+ * Strings pass through; maps pick `locale` → `fallback` → any value
169
+ * → empty. The empty fallback keeps the renderer from crashing on a
170
+ * malformed manifest while making the missing translation visible.
171
+ */
172
+ declare function resolveLocalized(value: LocalizedString | undefined, locale: string, fallback?: string): string;
173
+ interface ThemeRouteContext<P = Record<string, string>> {
174
+ params: Promise<P>;
175
+ }
176
+ interface ThemeModule {
177
+ /** Stable identifier — must match the directory name and the value
178
+ * stored as `theme.active`. */
179
+ name: string;
180
+ manifest: ThemeManifest;
181
+ /**
182
+ * Server components rendered by the dispatcher routes
183
+ * (`app/page.tsx`, `app/[slug]/page.tsx`, `app/tag/[tag]/page.tsx`).
184
+ * Each theme MUST provide Home; Post / Tag are recommended but
185
+ * optional (dispatcher 404s when missing).
186
+ */
187
+ components: {
188
+ Home: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
189
+ Post?: (ctx: ThemeRouteContext<{
190
+ slug: string;
191
+ }>) => Promise<unknown> | unknown;
192
+ Tag?: (ctx: ThemeRouteContext<{
193
+ tag: string;
194
+ }>) => Promise<unknown> | unknown;
195
+ };
196
+ /**
197
+ * Optional `generateMetadata` hooks called by the dispatcher. Same
198
+ * signature as the matching component's params.
199
+ */
200
+ metadata?: {
201
+ Home?: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
202
+ Post?: (ctx: ThemeRouteContext<{
203
+ slug: string;
204
+ }>) => Promise<unknown> | unknown;
205
+ Tag?: (ctx: ThemeRouteContext<{
206
+ tag: string;
207
+ }>) => Promise<unknown> | unknown;
208
+ };
209
+ /**
210
+ * Optional route handlers for /feed.xml and /sitemap.xml. The
211
+ * dispatcher returns 404 if a theme doesn't provide them.
212
+ */
213
+ routes?: {
214
+ feed?: (ctx: {
215
+ request: Request;
216
+ }) => Promise<Response>;
217
+ sitemap?: (ctx: {
218
+ request: Request;
219
+ }) => Promise<Response>;
220
+ };
221
+ }
222
+ declare function defineThemeModule(m: ThemeModule): ThemeModule;
223
+ /**
224
+ * Storage key used in KvStore. Prefix `theme.` keeps the namespace
225
+ * separate from `site.*` / `media.*` so unrelated tools can scan
226
+ * settings without colliding.
227
+ */
228
+ declare function themeSettingKey(fieldKey: string): string;
229
+ /**
230
+ * Reject malformed or potentially-injectable values before they reach
231
+ * the KvStore. Admin/editor are trusted but typos and copy-paste
232
+ * mistakes shouldn't be able to break a site's CSS or sneak `</style>`
233
+ * into the inline tag the loader emits.
234
+ *
235
+ * Returns the normalized value, or null if the input is rejected.
236
+ */
237
+ /**
238
+ * Split a stored color value into its light / dark components.
239
+ *
240
+ * Accepts two storage forms:
241
+ * - Single value: `oklch(...)` / `#abcdef` / etc. → `{ light: value, dark: null }`
242
+ * - Pair: `light-dark(L, D)` → `{ light: L, dark: D }`
243
+ *
244
+ * Splits on the top-level comma (depth-aware) so nested commas inside
245
+ * `rgb(...)` / `hsl(...)` don't trip the parser.
246
+ */
247
+ declare function parseColorPair(value: string): {
248
+ light: string;
249
+ dark: string | null;
250
+ };
251
+ /**
252
+ * Build the storage string for a color field. When `dark` is non-empty
253
+ * and differs from `light`, returns `light-dark(light, dark)`; otherwise
254
+ * returns the bare `light` value. The runtime emits the result verbatim
255
+ * into the inline `:root { --foo: <value> }` override; `light-dark()`
256
+ * is a Baseline-2024 CSS function so the browser picks per mode.
257
+ */
258
+ declare function formatColorPair(light: string, dark?: string | null): string;
259
+ declare function validateThemeValue(field: ThemeField, raw: unknown): string | null;
260
+ /** Parse a stored linkList JSON value into typed items. Tolerant: bad
261
+ * shapes resolve to []. Throwing here would cascade into rendering. */
262
+ declare function parseLinkList(raw: string | undefined | null): LinkListItem[];
263
+ declare function stringifyLinkList(items: ReadonlyArray<LinkListItem>): string;
264
+ /**
265
+ * Detect a `tag:<name>` URL form. Themes use this to render a list of
266
+ * posts under a heading instead of a literal link — useful for docs
267
+ * sidebars and category-style nav.
268
+ */
269
+ declare function isTagListUrl(url: string): {
270
+ tag: string;
271
+ } | null;
272
+ /**
273
+ * Resolve effective values for every manifest field, merging stored
274
+ * overrides on top of defaults. `stored` is the flat settings map keyed
275
+ * by `theme.{key}` — typically the output of `listSiteSettings()`
276
+ * filtered to theme entries.
277
+ */
278
+ declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<string, unknown>): Record<string, string>;
279
+
72
280
  type TrustLevel = 'untrusted' | 'trusted' | 'privileged';
281
+ /**
282
+ * Plugin capability declarations. The runtime uses this list for
283
+ * declaration-vs-implementation reconciliation warnings and (in later
284
+ * phases) for `allowCapabilities` gating in `cms.config.ts`.
285
+ *
286
+ * Phase 1 active capabilities:
287
+ * - `publicHead` / `publicBody`: descriptor-based head/body injection.
288
+ * - `metadata` / `eventHooks`: name-only declaration for existing surfaces.
289
+ *
290
+ * Reserved capabilities are accepted by the type so that plugins can
291
+ * declare future intent, but the runtime does nothing with them yet —
292
+ * each has its own RFP under `docs/tmp/`. Declaring a reserved
293
+ * capability today is harmless, but the runtime won't expose any new
294
+ * surface for it until the matching phase ships.
295
+ */
296
+ type PluginCapability = 'publicHead' | 'publicBody' | 'metadata' | 'eventHooks' | 'adminSettings' | 'schema' | 'writePublicAsset' | 'contentFields' | 'adminPage' | 'serverRoute' | 'secretSettings' | 'network' | 'scheduler' | 'storageWrite' | 'privilegedSystem';
297
+ /**
298
+ * Loading strategy for `script` / `inlineScript` descriptors.
299
+ *
300
+ * - `afterInteractive` (default): load after hydration, non-blocking.
301
+ * The runtime adds `async` for external scripts when neither
302
+ * `async` nor `defer` is set explicitly. `inlineScript` is emitted
303
+ * inline as-is in Phase 1 (strategy is informational only for
304
+ * inline; see comments in `plugin-head.ts`).
305
+ * - `lazyOnload`: defer load further. Phase 1 maps this to `defer`
306
+ * for external scripts; full lazy-load (next/script's idle
307
+ * scheduling) is a future enhancement.
308
+ *
309
+ * `beforeInteractive` is intentionally excluded — App Router does not
310
+ * support emitting plain `<script>` elements that block hydration from
311
+ * the runtime layer, and the spec defers that case to the future
312
+ * developer extension surface.
313
+ */
314
+ type ScriptStrategy = 'afterInteractive' | 'lazyOnload';
315
+ /**
316
+ * Context passed to `publicHead` / `publicBodyEnd`. Phase 1 carries
317
+ * only the site-wide config block; per-route and per-post context lands
318
+ * in Phase 4 (`plugin-per-post-rfp.md`). Plugin settings are read from
319
+ * the factory closure today — Phase 2 (`plugin-settings-rfp.md`) adds
320
+ * an admin-managed accessor here.
321
+ */
322
+ interface PluginPublicRenderContext {
323
+ site: Config['site'];
324
+ }
325
+ /**
326
+ * Descriptor returned by `publicHead()`. The runtime validates each
327
+ * entry (URL scheme denylist, `attrs` allowlist, id collision handling)
328
+ * and renders the surviving descriptors as React elements inside the
329
+ * root layout's `<head>`. Returning arbitrary `ReactNode` is
330
+ * intentionally not offered here — see
331
+ * `docs/architecture/08-plugin-architecture.md` §"Descriptor-based
332
+ * Head/Body Injection".
333
+ */
334
+ type PublicHeadDescriptor = {
335
+ type: 'script';
336
+ /** Element id; doubles as the duplicate-detection key. */
337
+ id?: string;
338
+ src: string;
339
+ strategy?: ScriptStrategy;
340
+ async?: boolean;
341
+ defer?: boolean;
342
+ /** Allow-listed attributes only (data-*, crossorigin, referrerpolicy, ...). */
343
+ attrs?: Record<string, string | boolean>;
344
+ } | {
345
+ type: 'inlineScript';
346
+ /** Required for duplicate detection and dev warnings. */
347
+ id: string;
348
+ body: string;
349
+ strategy?: ScriptStrategy;
350
+ /**
351
+ * Type-only reservation. CSP nonce resolution (including the
352
+ * planned `'auto'` mode) is deferred to a future RFP; Phase 1
353
+ * does not propagate this field to the rendered element.
354
+ */
355
+ nonce?: string;
356
+ } | {
357
+ type: 'meta';
358
+ name?: string;
359
+ property?: string;
360
+ content: string;
361
+ } | {
362
+ type: 'link';
363
+ rel: string;
364
+ href: string;
365
+ as?: string;
366
+ /** Mapped to React's `type` attribute. */
367
+ typeAttr?: string;
368
+ } | {
369
+ type: 'noscript';
370
+ id?: string;
371
+ /** Raw HTML emitted inside `<noscript>`. */
372
+ html: string;
373
+ };
374
+ /**
375
+ * Descriptor returned by `publicBodyEnd()`. Supports the same
376
+ * `script` / `inlineScript` / `noscript` variants as the head, plus
377
+ * the body-only `iframe` variant (used by Google Tag Manager's
378
+ * `<noscript>`-style fallback frame, chat widgets, etc.).
379
+ */
380
+ type PublicBodyDescriptor = Extract<PublicHeadDescriptor, {
381
+ type: 'script' | 'inlineScript' | 'noscript';
382
+ }> | {
383
+ type: 'iframe';
384
+ id?: string;
385
+ src: string;
386
+ title?: string;
387
+ width?: number;
388
+ height?: number;
389
+ attrs?: Record<string, string | boolean>;
390
+ };
73
391
  /**
74
392
  * Metadata-like object that maps cleanly onto Next.js `Metadata`. We keep
75
393
  * the shape framework-agnostic so the core type doesn't depend on Next.js.
@@ -161,6 +479,28 @@ interface AmplessPlugin {
161
479
  /** Plugin API version. Currently 1; future versions will be additive. */
162
480
  apiVersion: 1;
163
481
  trust_level: TrustLevel;
482
+ /**
483
+ * Stable per-install namespace. Defaults to `name` when omitted.
484
+ * Distinguishes multiple instances of the same plugin (e.g. two GTM
485
+ * containers, two GA4 measurement IDs). Multi-instance run-time
486
+ * validation lands in Phase 3; Phase 1 only adds the field so plugin
487
+ * authors can author against it now.
488
+ */
489
+ instanceId?: string;
490
+ /**
491
+ * Human-readable label for admin UI surfaces (Phase 2 onward).
492
+ * Plain string or per-locale map — see `LocalizedString`.
493
+ */
494
+ displayName?: LocalizedString;
495
+ /**
496
+ * Declared capability list. The runtime uses this for
497
+ * declaration-vs-implementation warnings (e.g. a plugin that
498
+ * declares `publicBody` but defines no `publicBodyEnd`); later
499
+ * phases will gate dangerous capabilities through `cms.config.ts`
500
+ * `allowCapabilities`. Existing plugins that omit this field
501
+ * continue to work unchanged.
502
+ */
503
+ capabilities?: readonly PluginCapability[];
164
504
  /** Async event hooks. Run in trust_level-matched Lambda. */
165
505
  hooks?: {
166
506
  [K in EventType]?: PluginEventHandler<K>;
@@ -174,6 +514,20 @@ interface AmplessPlugin {
174
514
  * Site-level metadata (root layout). Returned per request.
175
515
  */
176
516
  siteMetadata?(site: Config['site']): PluginMetadata;
517
+ /**
518
+ * Declarative head injection. Returns a list of validated
519
+ * descriptors (script / inlineScript / meta / link / noscript). The
520
+ * runtime collects every plugin's contribution at render time, runs
521
+ * URL scheme + attrs validation, then emits React elements inside
522
+ * the root layout's `<head>`. See `PublicHeadDescriptor`.
523
+ */
524
+ publicHead?(ctx: PluginPublicRenderContext): readonly PublicHeadDescriptor[];
525
+ /**
526
+ * Same shape as `publicHead`, but the result is appended at the
527
+ * end of `<body>` and additionally supports the `iframe` variant
528
+ * (GTM no-script fallback frame, chat widgets, ...).
529
+ */
530
+ publicBodyEnd?(ctx: PluginPublicRenderContext): readonly PublicBodyDescriptor[];
177
531
  /**
178
532
  * Dynamic OG image renderer. The dispatcher route (e.g.
179
533
  * `app/og/[slug]/route.ts`) reads this and feeds the element into
@@ -246,9 +600,33 @@ type CacheStrategy = 'auto' | 'deep' | 'hot';
246
600
  * are free to store their own per-post state here (e.g. SEO overrides,
247
601
  * feature flags, A/B variants).
248
602
  */
603
+ /**
604
+ * Per-file metadata recorded on a static post. The static route
605
+ * reads this to decide whether to stream the bytes back through
606
+ * Lambda (small files → cached by CloudFront) or to 302-redirect
607
+ * to a presigned URL (large files → bypass the Lambda response
608
+ * size envelope). Populated by `upload_static_bundle` /
609
+ * `commit_static_post` at upload time so the read path never
610
+ * issues a HEAD round-trip.
611
+ *
612
+ * `body.files` (the manifest's flat list) stays the source of truth
613
+ * for "what's in the bundle"; this map is purely a delivery hint and
614
+ * may be sparse when older bundles predate the migration.
615
+ */
616
+ interface StaticPostFileMeta {
617
+ size: number;
618
+ mimeType: string;
619
+ }
249
620
  interface PostMetadata {
250
621
  no_layout?: boolean;
251
622
  cache?: CacheStrategy;
623
+ /**
624
+ * For `format: 'static'` posts only. Keyed by the bundle-relative
625
+ * path (same shape as `body.files` entries). Older bundles may
626
+ * lack this map — readers MUST treat a missing entry as
627
+ * "fall back to a HEAD lookup".
628
+ */
629
+ files?: Record<string, StaticPostFileMeta>;
252
630
  [key: string]: unknown;
253
631
  }
254
632
  interface Post {
@@ -281,12 +659,25 @@ interface Page {
281
659
  status: PostStatus;
282
660
  publishedAt?: string;
283
661
  }
662
+ /**
663
+ * Asset metadata recorded on the Media row. Currently the only
664
+ * well-known key is `etag` (the S3 object ETag, captured at upload
665
+ * time so the media-proxy route can emit it back to the client
666
+ * without a HEAD round-trip). Free-form by design — themes and
667
+ * plugins can add their own keys (image dimensions, EXIF strip
668
+ * status, etc.) without a schema change.
669
+ */
670
+ interface MediaMetadata {
671
+ etag?: string;
672
+ [key: string]: unknown;
673
+ }
284
674
  interface Media {
285
675
  mediaId: string;
286
676
  src: string;
287
677
  mimeType: string;
288
678
  size: number;
289
679
  delivery: 'nextjs' | 's3-direct';
680
+ metadata?: MediaMetadata;
290
681
  }
291
682
  type ImageDisplay = 'inline' | 'lightbox';
292
683
  /**
@@ -500,9 +891,8 @@ declare function escapeXml(s: string): string;
500
891
  * from DynamoDB (the path used by the trusted processor and the
501
892
  * MCP Lambda).
502
893
  *
503
- * `decodeAwsJson` tolerates both: pass strings through `JSON.parse`,
504
- * everything else as-is, fall back to the raw string on parse errors
505
- * so bare-string rows aren't lost.
894
+ * `decodeAwsJson` handles both: pass strings through `JSON.parse`,
895
+ * everything else as-is.
506
896
  */
507
897
  /**
508
898
  * Serialise a value for an AWSJSON variable. `undefined` / `null` both
@@ -513,7 +903,7 @@ declare function encodeAwsJson(value: unknown): string;
513
903
  /**
514
904
  * Deserialise an AWSJSON value from a GraphQL / DynamoDB read.
515
905
  * Tolerates both the wire-string shape and the auto-unmarshalled
516
- * native value. Returns the raw string when `JSON.parse` rejects it.
906
+ * native value. Throws if the string is not valid JSON.
517
907
  */
518
908
  declare function decodeAwsJson(value: unknown): unknown;
519
909
 
@@ -627,214 +1017,6 @@ declare function pickDefaultEntrypoint(files: readonly {
627
1017
  path: string;
628
1018
  }[]): string;
629
1019
 
630
- type ThemeFieldType = 'color' | 'text' | 'select' | 'image' | 'length' | 'fontFamily' | 'linkList';
631
- /**
632
- * Single entry in a `linkList` field. Stored as part of a JSON array
633
- * under the field's storage key. `url` may be:
634
- * - a relative path (`/about`)
635
- * - an absolute URL (`https://example.com`)
636
- * - a tag reference (`tag:guide`) — themes interpret this as
637
- * "expand to a list of posts with this tag" rather than rendering
638
- * a literal link.
639
- */
640
- interface LinkListItem {
641
- label: string;
642
- url: string;
643
- }
644
- /**
645
- * A user-facing string in a manifest. Either a plain string (rendered
646
- * as-is, regardless of locale) or a per-locale map (the renderer picks
647
- * the active locale, falling back to `en`, then to any value).
648
- *
649
- * Themes that ship in a single language can keep these as plain
650
- * strings. The default themes use the map form so the same manifest
651
- * works for both built-in dictionaries.
652
- */
653
- type LocalizedString = string | Record<string, string>;
654
- interface ThemeFieldBase {
655
- /** Storage key. Persisted as `theme.{key}` in site settings. */
656
- key: string;
657
- label: LocalizedString;
658
- description?: LocalizedString;
659
- /** Optional UI grouping (e.g. 'Colors', 'Typography', 'Branding'). */
660
- group?: LocalizedString;
661
- /** Used when no override is set. Always a string for storage uniformity. */
662
- default: string;
663
- /**
664
- * If set, the loader injects `${cssVar}: ${value}` into a `:root`
665
- * style block on every public page, so CSS rules using
666
- * `var(${cssVar})` pick up overrides at render time.
667
- *
668
- * Fields without `cssVar` (e.g. logo URL, header tagline) are exposed
669
- * to template code via `loadThemeConfig()` instead.
670
- */
671
- cssVar?: string;
672
- }
673
- interface ThemeColorField extends ThemeFieldBase {
674
- type: 'color';
675
- }
676
- interface ThemeTextField extends ThemeFieldBase {
677
- type: 'text';
678
- maxLength?: number;
679
- }
680
- interface ThemeSelectField extends ThemeFieldBase {
681
- type: 'select';
682
- options: ReadonlyArray<{
683
- value: string;
684
- label: LocalizedString;
685
- }>;
686
- }
687
- interface ThemeImageField extends ThemeFieldBase {
688
- type: 'image';
689
- }
690
- interface ThemeLengthField extends ThemeFieldBase {
691
- type: 'length';
692
- }
693
- interface ThemeFontFamilyField extends ThemeFieldBase {
694
- type: 'fontFamily';
695
- options: ReadonlyArray<{
696
- value: string;
697
- label: LocalizedString;
698
- }>;
699
- }
700
- /**
701
- * A repeatable list of {label, url} entries — used for nav menus,
702
- * footer link sets, sidebar groups, etc. Stored in KvStore as a JSON
703
- * string so it fits the existing `string`-valued site-settings cache.
704
- *
705
- * `default` is declared as a plain array for ergonomics; the loader
706
- * stringifies it on the fly so manifest authors don't have to call
707
- * JSON.stringify by hand.
708
- */
709
- interface ThemeLinkListField extends Omit<ThemeFieldBase, 'default' | 'cssVar'> {
710
- type: 'linkList';
711
- default: ReadonlyArray<LinkListItem>;
712
- /** Cap admin-supplied list length. Default 50. */
713
- maxItems?: number;
714
- }
715
- type ThemeField = ThemeColorField | ThemeTextField | ThemeSelectField | ThemeImageField | ThemeLengthField | ThemeFontFamilyField | ThemeLinkListField;
716
- interface ThemeManifest {
717
- /** Theme directory name (`themes/<name>/`). */
718
- name: string;
719
- label: LocalizedString;
720
- description?: LocalizedString;
721
- fields: ReadonlyArray<ThemeField>;
722
- }
723
- declare function defineTheme(m: ThemeManifest): ThemeManifest;
724
- /**
725
- * Resolve a `LocalizedString` to a plain string for display.
726
- * Strings pass through; maps pick `locale` → `fallback` → any value
727
- * → empty. The empty fallback keeps the renderer from crashing on a
728
- * malformed manifest while making the missing translation visible.
729
- */
730
- declare function resolveLocalized(value: LocalizedString | undefined, locale: string, fallback?: string): string;
731
- interface ThemeRouteContext<P = Record<string, string>> {
732
- params: Promise<P>;
733
- }
734
- interface ThemeModule {
735
- /** Stable identifier — must match the directory name and the value
736
- * stored as `theme.active`. */
737
- name: string;
738
- manifest: ThemeManifest;
739
- /**
740
- * Server components rendered by the dispatcher routes
741
- * (`app/page.tsx`, `app/[slug]/page.tsx`, `app/tag/[tag]/page.tsx`).
742
- * Each theme MUST provide Home; Post / Tag are recommended but
743
- * optional (dispatcher 404s when missing).
744
- */
745
- components: {
746
- Home: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
747
- Post?: (ctx: ThemeRouteContext<{
748
- slug: string;
749
- }>) => Promise<unknown> | unknown;
750
- Tag?: (ctx: ThemeRouteContext<{
751
- tag: string;
752
- }>) => Promise<unknown> | unknown;
753
- };
754
- /**
755
- * Optional `generateMetadata` hooks called by the dispatcher. Same
756
- * signature as the matching component's params.
757
- */
758
- metadata?: {
759
- Home?: (ctx: ThemeRouteContext) => Promise<unknown> | unknown;
760
- Post?: (ctx: ThemeRouteContext<{
761
- slug: string;
762
- }>) => Promise<unknown> | unknown;
763
- Tag?: (ctx: ThemeRouteContext<{
764
- tag: string;
765
- }>) => Promise<unknown> | unknown;
766
- };
767
- /**
768
- * Optional route handlers for /feed.xml and /sitemap.xml. The
769
- * dispatcher returns 404 if a theme doesn't provide them.
770
- */
771
- routes?: {
772
- feed?: (ctx: {
773
- request: Request;
774
- }) => Promise<Response>;
775
- sitemap?: (ctx: {
776
- request: Request;
777
- }) => Promise<Response>;
778
- };
779
- }
780
- declare function defineThemeModule(m: ThemeModule): ThemeModule;
781
- /**
782
- * Storage key used in KvStore. Prefix `theme.` keeps the namespace
783
- * separate from `site.*` / `media.*` so unrelated tools can scan
784
- * settings without colliding.
785
- */
786
- declare function themeSettingKey(fieldKey: string): string;
787
- /**
788
- * Reject malformed or potentially-injectable values before they reach
789
- * the KvStore. Admin/editor are trusted but typos and copy-paste
790
- * mistakes shouldn't be able to break a site's CSS or sneak `</style>`
791
- * into the inline tag the loader emits.
792
- *
793
- * Returns the normalized value, or null if the input is rejected.
794
- */
795
- /**
796
- * Split a stored color value into its light / dark components.
797
- *
798
- * Accepts two storage forms:
799
- * - Single value: `oklch(...)` / `#abcdef` / etc. → `{ light: value, dark: null }`
800
- * - Pair: `light-dark(L, D)` → `{ light: L, dark: D }`
801
- *
802
- * Splits on the top-level comma (depth-aware) so nested commas inside
803
- * `rgb(...)` / `hsl(...)` don't trip the parser.
804
- */
805
- declare function parseColorPair(value: string): {
806
- light: string;
807
- dark: string | null;
808
- };
809
- /**
810
- * Build the storage string for a color field. When `dark` is non-empty
811
- * and differs from `light`, returns `light-dark(light, dark)`; otherwise
812
- * returns the bare `light` value. The runtime emits the result verbatim
813
- * into the inline `:root { --foo: <value> }` override; `light-dark()`
814
- * is a Baseline-2024 CSS function so the browser picks per mode.
815
- */
816
- declare function formatColorPair(light: string, dark?: string | null): string;
817
- declare function validateThemeValue(field: ThemeField, raw: unknown): string | null;
818
- /** Parse a stored linkList JSON value into typed items. Tolerant: bad
819
- * shapes resolve to []. Throwing here would cascade into rendering. */
820
- declare function parseLinkList(raw: string | undefined | null): LinkListItem[];
821
- declare function stringifyLinkList(items: ReadonlyArray<LinkListItem>): string;
822
- /**
823
- * Detect a `tag:<name>` URL form. Themes use this to render a list of
824
- * posts under a heading instead of a literal link — useful for docs
825
- * sidebars and category-style nav.
826
- */
827
- declare function isTagListUrl(url: string): {
828
- tag: string;
829
- } | null;
830
- /**
831
- * Resolve effective values for every manifest field, merging stored
832
- * overrides on top of defaults. `stored` is the flat settings map keyed
833
- * by `theme.{key}` — typically the output of `listSiteSettings()`
834
- * filtered to theme entries.
835
- */
836
- declare function resolveThemeValues(manifest: ThemeManifest, stored: Record<string, unknown>): Record<string, string>;
837
-
838
1020
  declare const VERSION = "0.0.1";
839
1021
 
840
- export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type CacheConfig, type CacheStrategy, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, type DateFormat, type EventPayloadOf, type EventType, type ExtractedFile, type FieldDefinition, type FieldType, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type LocalizedString, MAX_BUNDLE_BYTES, type Media, type MediaEventPayload, type MediaEventType, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, type Page, type PluginEventHandler, type PluginMetadata, type PluginRuntimeContext, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostMetadata, type PostStatus, type PostsProvider, type Role, SITE_CONFIG_PK, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StreamEventName, TEXT_EXTENSIONS, type ThemeColorField, type ThemeField, type ThemeFieldType, type ThemeFontFamilyField, type ThemeImageField, type ThemeLengthField, type ThemeLinkListField, type ThemeManifest, type ThemeModule, type ThemeRouteContext, type ThemeSelectField, type ThemeTextField, type TrustLevel, VERSION, type ValidationIssue, bundlePrefix, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isTagListUrl, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validateThemeValue };
1022
+ export { type AmplessEvent, type AmplessPlugin, type AuthContext, type BundleExtractResult, type CacheConfig, type CacheStrategy, type Config, type ContentEventPayload, type ContentEventType, type ContentFormat, type ContentTypeDefinition, type CreatePostInput, DEFAULT_ENTRYPOINT, type DateFormat, type EventPayloadOf, type EventType, type ExtractedFile, type FieldDefinition, type FieldType, type ImageDisplay, type KvItem, type KvStore, type LinkListItem, type ListOptions, type LocalizedString, MAX_BUNDLE_BYTES, type Media, type MediaEventPayload, type MediaEventType, type MediaMetadata, type MediaProcessingDefaults, type OgImageConfig, type OgImageFont, type OgImageRenderContext, type Page, type PluginCapability, type PluginEventHandler, type PluginMetadata, type PluginPublicRenderContext, type PluginRuntimeContext, type Post, type PostIndexEventPayload, type PostIndexEventType, type PostMetadata, type PostStatus, type PostsProvider, type PublicBodyDescriptor, type PublicHeadDescriptor, type Role, SITE_CONFIG_PK, type ScriptStrategy, type SiteSettingsEventPayload, type SiteSettingsEventType, type StaticPostBody, type StaticPostFileMeta, type StreamEventName, TEXT_EXTENSIONS, type ThemeColorField, type ThemeField, type ThemeFieldType, type ThemeFontFamilyField, type ThemeImageField, type ThemeLengthField, type ThemeLinkListField, type ThemeManifest, type ThemeModule, type ThemeRouteContext, type ThemeSelectField, type ThemeTextField, type TrustLevel, VERSION, type ValidationIssue, bundlePrefix, createPost, decodeAwsJson, defineConfig, definePlugin, defineSchema, defineTheme, defineThemeModule, deletePost, deleteSiteSetting, detectContentEvents, encodeAwsJson, escapeXml, extractFirstImageUrl, findAbsolutePathRefs, flattenSettings, formatColorPair, formatDate, formatPublicAssetUrl, getKvStore, getPost, getPostById, getSiteSetting, hasKvStore, hasPostsProvider, isTagListUrl, listPosts, listSiteSettings, mimeTypeFor, parseColorPair, parseLinkList, pickDefaultEntrypoint, resolveLocalized, resolveThemeValues, setKvStore, setPostsProvider, setSiteSetting, stringifyLinkList, stripCommonPrefix, themeSettingKey, unflattenSettings, updatePost, validateBundle, validateBundlePath, validateThemeValue };
package/dist/index.js CHANGED
@@ -202,11 +202,7 @@ function encodeAwsJson(value) {
202
202
  }
203
203
  function decodeAwsJson(value) {
204
204
  if (typeof value !== "string") return value;
205
- try {
206
- return JSON.parse(value);
207
- } catch {
208
- return value;
209
- }
205
+ return JSON.parse(value);
210
206
  }
211
207
 
212
208
  // src/storage.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ampless",
3
- "version": "1.0.0-alpha.15",
3
+ "version": "1.0.0-alpha.17",
4
4
  "description": "Serverless CMS for AWS Amplify",
5
5
  "license": "MIT",
6
6
  "type": "module",