@ampless/runtime 1.0.0-alpha.23 → 1.0.0-alpha.24

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
@@ -123,19 +123,50 @@ interface SeoApi {
123
123
  siteMetadata(): Promise<Metadata>;
124
124
  }
125
125
 
126
+ /**
127
+ * Per-instance flat map of stored field values, keyed by the field's
128
+ * `key` (not the full `plugins.<instanceId>.<key>` SK). The keys
129
+ * here mirror the field manifest so callers can hand the map
130
+ * directly to `resolvePluginSettings`.
131
+ */
132
+ type PluginSettingsSnapshot = Map<string, Record<string, unknown>>;
133
+ interface PluginSettingsApi {
134
+ /**
135
+ * Fetch all `plugins.*` settings from the public cache and bucket
136
+ * them by `instanceId`. Returns an empty Map on any failure mode
137
+ * (storage unconfigured, 404, JSON parse error) — callers fall
138
+ * back to manifest defaults so the layout never crashes when the
139
+ * cache is missing.
140
+ */
141
+ loadAll(): Promise<PluginSettingsSnapshot>;
142
+ }
143
+ declare function createPluginSettings(storage: StorageApi): PluginSettingsApi;
144
+
126
145
  interface PluginHeadApi {
127
- /** React children safe to drop into `<head>`. */
128
- renderHead(): ReactNode;
146
+ /**
147
+ * React children safe to drop into `<head>`. Async because admin-
148
+ * managed settings are read from S3 on the first call per request.
149
+ * Within a single request both `renderHead` and `renderBodyEnd` share
150
+ * the same fetched snapshot via Next.js fetch dedup on the
151
+ * `site-settings` cache tag.
152
+ */
153
+ renderHead(): Promise<ReactNode>;
129
154
  /** React children safe to drop just before `</body>`. */
130
- renderBodyEnd(): ReactNode;
155
+ renderBodyEnd(): Promise<ReactNode>;
131
156
  }
132
157
  /**
133
158
  * Create the head/body renderer for a `Config`. The constructor-time
134
159
  * pass logs a single dev warning when two plugins share an
135
160
  * `instanceId ?? name`; everything else happens at render time so
136
161
  * descriptors reflect per-request site config.
162
+ *
163
+ * `pluginSettings` (Phase 2) is the runtime accessor that pulls
164
+ * admin-managed `settings.public` values from the S3 site-settings
165
+ * cache. Within a single request we fetch once via `loadAll()` and
166
+ * bind a per-plugin `ctx.setting(key)` accessor before invoking
167
+ * either `publicHead` or `publicBodyEnd`.
137
168
  */
138
- declare function createPluginHead(cmsConfig: Config): PluginHeadApi;
169
+ declare function createPluginHead(cmsConfig: Config, pluginSettings: PluginSettingsApi): PluginHeadApi;
139
170
 
140
171
  interface ThemesRegistry {
141
172
  /** Map of theme name → loaded theme module. */
@@ -351,8 +382,8 @@ interface Ampless {
351
382
  loadThemeConfig(): Promise<EffectiveThemeConfig>;
352
383
  postMetadata(post: Post): Promise<Metadata>;
353
384
  siteMetadata(): Promise<Metadata>;
354
- publicHead(): ReactNode;
355
- publicBodyEnd(): ReactNode;
385
+ publicHead(): Promise<ReactNode>;
386
+ publicBodyEnd(): Promise<ReactNode>;
356
387
  renderBody(post: Post): string;
357
388
  renderThemeCss(cssVars: Record<string, string>): string;
358
389
  publicAssetUrl(key: string): string;
@@ -368,6 +399,7 @@ interface Ampless {
368
399
  readonly themeConfig: ThemeConfigApi;
369
400
  readonly storageApi: StorageApi;
370
401
  readonly pluginHead: PluginHeadApi;
402
+ readonly pluginSettings: PluginSettingsApi;
371
403
  }
372
404
  /**
373
405
  * Wire up the ampless runtime from user-supplied config blobs. The
@@ -378,4 +410,4 @@ interface Ampless {
378
410
  */
379
411
  declare function createAmpless(opts: CreateAmplessOpts): Ampless;
380
412
 
381
- export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PostsApi, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, createAmpless, createPluginHead, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
413
+ export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PluginSettingsApi, type PluginSettingsSnapshot, type PostsApi, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, createAmpless, createPluginHead, createPluginSettings, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
package/dist/index.js CHANGED
@@ -230,6 +230,10 @@ import {
230
230
  Fragment,
231
231
  createElement
232
232
  } from "react";
233
+ import {
234
+ isValidPluginKey,
235
+ resolvePluginSettings
236
+ } from "ampless";
233
237
  function isPlugin2(p) {
234
238
  return typeof p === "object" && p !== null && "apiVersion" in p;
235
239
  }
@@ -419,11 +423,25 @@ function dedupeAndKey(entries) {
419
423
  }
420
424
  return kept;
421
425
  }
422
- function collectFor(plugins, ctx, surface, renderOne) {
426
+ function makeCtx(plugin, site, snapshot) {
427
+ const instanceId = plugin.instanceId ?? plugin.name;
428
+ const stored = snapshot.get(instanceId) ?? {};
429
+ const resolved = resolvePluginSettings(plugin.settings, stored);
430
+ return {
431
+ site,
432
+ setting(key) {
433
+ if (!isValidPluginKey(key)) return void 0;
434
+ const v = resolved[key];
435
+ return v === void 0 ? void 0 : v;
436
+ }
437
+ };
438
+ }
439
+ function collectFor(plugins, site, snapshot, surface, renderOne) {
423
440
  const entries = [];
424
441
  for (const plugin of plugins) {
425
442
  const factory = surface(plugin);
426
443
  if (!factory) continue;
444
+ const ctx = makeCtx(plugin, site, snapshot);
427
445
  let descriptors;
428
446
  try {
429
447
  descriptors = factory.call(plugin, ctx) ?? [];
@@ -443,18 +461,35 @@ function collectFor(plugins, ctx, surface, renderOne) {
443
461
  const keyed = dedupeAndKey(entries);
444
462
  return createElement(Fragment, null, ...keyed);
445
463
  }
446
- function createPluginHead(cmsConfig) {
464
+ function createPluginHead(cmsConfig, pluginSettings) {
447
465
  const plugins = (cmsConfig.plugins ?? []).filter(isPlugin2);
466
+ const validPlugins = [];
448
467
  const seenNamespaces = /* @__PURE__ */ new Set();
449
468
  for (const plugin of plugins) {
450
469
  const ns = plugin.instanceId ?? plugin.name;
451
470
  const label = plugin.instanceId ? `${plugin.name}#${plugin.instanceId}` : plugin.name;
471
+ if (!isValidPluginKey(ns)) {
472
+ warn(
473
+ `${label}: plugin namespace "${ns}" violates ${`/^[a-zA-Z0-9_-]+$/`}. Use a simple identifier (letters / digits / "-" / "_"). Skipping plugin.`
474
+ );
475
+ continue;
476
+ }
452
477
  if (seenNamespaces.has(ns)) {
453
478
  warn(
454
479
  `duplicate plugin namespace "${ns}" detected in cms.config.plugins. Set distinct \`instanceId\` on each instance to disambiguate.`
455
480
  );
456
481
  }
457
482
  seenNamespaces.add(ns);
483
+ if (plugin.settings?.public) {
484
+ for (const field of plugin.settings.public) {
485
+ if (!isValidPluginKey(field.key)) {
486
+ warn(
487
+ `${label}: settings.public field key "${field.key}" violates ${`/^[a-zA-Z0-9_-]+$/`}. The field will not be readable through ctx.setting(). Rename the field key.`
488
+ );
489
+ }
490
+ }
491
+ }
492
+ validPlugins.push(plugin);
458
493
  const caps = plugin.capabilities;
459
494
  if (caps) {
460
495
  if (caps.includes("publicHead") && !plugin.publicHead) {
@@ -480,18 +515,22 @@ function createPluginHead(cmsConfig) {
480
515
  }
481
516
  }
482
517
  return {
483
- renderHead() {
518
+ async renderHead() {
519
+ const snapshot = await pluginSettings.loadAll();
484
520
  return collectFor(
485
- plugins,
486
- { site: cmsConfig.site },
521
+ validPlugins,
522
+ cmsConfig.site,
523
+ snapshot,
487
524
  (p) => p.publicHead,
488
525
  renderHeadDescriptor
489
526
  );
490
527
  },
491
- renderBodyEnd() {
528
+ async renderBodyEnd() {
529
+ const snapshot = await pluginSettings.loadAll();
492
530
  return collectFor(
493
- plugins,
494
- { site: cmsConfig.site },
531
+ validPlugins,
532
+ cmsConfig.site,
533
+ snapshot,
495
534
  (p) => p.publicBodyEnd,
496
535
  renderBodyDescriptor
497
536
  );
@@ -499,6 +538,44 @@ function createPluginHead(cmsConfig) {
499
538
  };
500
539
  }
501
540
 
541
+ // src/plugin-settings.ts
542
+ import { unflattenSettings as unflattenSettings2 } from "ampless";
543
+ function createPluginSettings(storage) {
544
+ return {
545
+ async loadAll() {
546
+ const out = /* @__PURE__ */ new Map();
547
+ if (!storage.isStorageConfigured()) return out;
548
+ let url;
549
+ try {
550
+ url = storage.publicAssetUrl("public/site-settings.json");
551
+ } catch {
552
+ return out;
553
+ }
554
+ let flat;
555
+ try {
556
+ const res = await fetch(url, {
557
+ // Same revalidate + tag as site-settings.ts so a single
558
+ // request to the public route hits the JSON once and both
559
+ // helpers share the cached body.
560
+ next: { revalidate: 60, tags: ["site-settings"] }
561
+ });
562
+ if (!res.ok) return out;
563
+ flat = await res.json();
564
+ } catch {
565
+ return out;
566
+ }
567
+ const nested = unflattenSettings2(flat);
568
+ const pluginsBlock = nested.plugins;
569
+ if (!pluginsBlock || typeof pluginsBlock !== "object") return out;
570
+ for (const [instanceId, block] of Object.entries(pluginsBlock)) {
571
+ if (!block || typeof block !== "object" || Array.isArray(block)) continue;
572
+ out.set(instanceId, { ...block });
573
+ }
574
+ return out;
575
+ }
576
+ };
577
+ }
578
+
502
579
  // src/theme-active.ts
503
580
  import { headers } from "next/headers";
504
581
  function createThemeActive(registry, storage) {
@@ -931,7 +1008,8 @@ function createAmpless(opts) {
931
1008
  const media = createMediaApi(outputs);
932
1009
  const settings = createSiteSettings(cmsConfig, storage);
933
1010
  const seo = createSeo(cmsConfig, settings);
934
- const pluginHead = createPluginHead(cmsConfig);
1011
+ const pluginSettings = createPluginSettings(storage);
1012
+ const pluginHead = createPluginHead(cmsConfig, pluginSettings);
935
1013
  const themeActive = createThemeActive(themes, storage);
936
1014
  const themeConfig = createThemeConfig(themeActive, storage);
937
1015
  return {
@@ -961,7 +1039,8 @@ function createAmpless(opts) {
961
1039
  themeActive,
962
1040
  themeConfig,
963
1041
  storageApi: storage,
964
- pluginHead
1042
+ pluginHead,
1043
+ pluginSettings
965
1044
  };
966
1045
  }
967
1046
  export {
@@ -970,6 +1049,7 @@ export {
970
1049
  _resetStreamS3Cache,
971
1050
  createAmpless,
972
1051
  createPluginHead,
1052
+ createPluginSettings,
973
1053
  htmlToMarkdown,
974
1054
  markdownToHtml,
975
1055
  renderBody,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-alpha.23",
3
+ "version": "1.0.0-alpha.24",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,8 +49,8 @@
49
49
  "lucide-react": "^1.16.0",
50
50
  "marked": "^18.0.4",
51
51
  "tailwind-merge": "^3.6.0",
52
- "ampless": "1.0.0-alpha.17",
53
- "@ampless/plugin-og-image": "0.2.0-alpha.17"
52
+ "ampless": "1.0.0-alpha.18",
53
+ "@ampless/plugin-og-image": "0.2.0-alpha.18"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "next": "^15 || ^16",