@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 +39 -7
- package/dist/index.js +90 -10
- package/package.json +3 -3
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
|
-
/**
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
|
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.
|
|
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.
|
|
53
|
-
"@ampless/plugin-og-image": "0.2.0-alpha.
|
|
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",
|