@saena-io/content 0.1.0 → 0.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saena-io/content",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -21,8 +21,8 @@
21
21
  "clean": "rm -rf dist .turbo"
22
22
  },
23
23
  "dependencies": {
24
- "@saena-io/plugin-sdk": "workspace:*",
25
- "@saena-io/ui": "workspace:*",
24
+ "@saena-io/plugin-sdk": "^0.1.0",
25
+ "@saena-io/ui": "^0.1.1",
26
26
  "drizzle-orm": "^0.45.2",
27
27
  "react": "^19.2.6",
28
28
  "zod": "^4.4.3"
@@ -31,10 +31,7 @@
31
31
  "@types/react": "^19",
32
32
  "drizzle-kit": "^0.31.10"
33
33
  },
34
- "files": [
35
- "src",
36
- "drizzle"
37
- ],
34
+ "files": ["src", "drizzle"],
38
35
  "publishConfig": {
39
36
  "access": "public"
40
37
  }
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { FieldDecl, FieldPath } from '../field';
3
+ import { singletonMap } from '../singleton';
3
4
 
4
5
  // The React half of the field-type registry: a field-type id → its admin editor. Built-in editors register on
5
6
  // import (./editors); custom field types register their editor the same way. The form engine renders
@@ -21,7 +22,7 @@ export interface AdminEditorProps<S = unknown> {
21
22
  }
22
23
  export type AdminEditor<S = unknown> = (props: AdminEditorProps<S>) => ReactNode;
23
24
 
24
- const editors = new Map<string, AdminEditor>();
25
+ const editors = singletonMap<string, AdminEditor>('content.editors');
25
26
 
26
27
  /** Register the admin editor for a field-type id (built-in or custom). */
27
28
  export function registerEditor<S>(id: string, editor: AdminEditor<S>): void {
package/src/define.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { FieldDecl, InferValue } from './field';
2
+ import { singletonMap } from './singleton';
2
3
 
3
4
  // Section + page DEFINITIONS — the React-free half of "structure is code" (ADR-0008). A section author declares
4
5
  // a section's id, label, and field schema here (the schema drives admin form + validation + i18n + the public
@@ -11,7 +12,7 @@ export interface SectionDef<S extends FieldDecl = FieldDecl> {
11
12
  schema: S;
12
13
  }
13
14
 
14
- const sections = new Map<string, SectionDef>();
15
+ const sections = singletonMap<string, SectionDef>('content.sections');
15
16
 
16
17
  /** Declare a content section's schema (registers it for the admin form, the server service, and key derivation). */
17
18
  export function defineSection<S extends FieldDecl>(def: SectionDef<S>): SectionDef<S> {
@@ -45,7 +46,7 @@ export interface CollectionDef<S extends FieldDecl = FieldDecl> {
45
46
  slug?: { from?: string };
46
47
  }
47
48
 
48
- const collections = new Map<string, CollectionDef>();
49
+ const collections = singletonMap<string, CollectionDef>('content.collections');
49
50
 
50
51
  /** Declare a routed collection (registers it for the admin manager, the server service, and key derivation). */
51
52
  export function defineCollection<S extends FieldDecl>(def: CollectionDef<S>): CollectionDef<S> {
@@ -76,7 +77,7 @@ export interface PageDef {
76
77
  sections: string[];
77
78
  }
78
79
 
79
- const pages = new Map<string, PageDef>();
80
+ const pages = singletonMap<string, PageDef>('content.pages');
80
81
 
81
82
  /** Declare a page as a fixed, ordered composition of sections (the admin reads this tree read-only). */
82
83
  export function definePage(def: PageDef): PageDef {
@@ -1,3 +1,4 @@
1
+ import { singletonMap } from '../singleton';
1
2
  import type { FieldDecl, FieldType } from './types';
2
3
 
3
4
  // The React-free field-type registry: id → runtime behaviour (empty/validate/leaves/hydrate). Built-ins
@@ -5,7 +6,7 @@ import type { FieldDecl, FieldType } from './types';
5
6
  // through `fieldType(id)`, so the whole schema tree is traversable from any value. The React editor registry
6
7
  // (id → AdminEditor) is a separate, parallel map in ../admin — same id, different world.
7
8
 
8
- const registry = new Map<string, FieldType>();
9
+ const registry = singletonMap<string, FieldType>('content.fieldTypes');
9
10
 
10
11
  /** Register a field type's runtime behaviour. Idempotent by id (last registration wins). */
11
12
  export function registerFieldType(type: FieldType): void {
@@ -2,6 +2,7 @@ import { type AssetRef, IMAGE_WIDTH_LADDER } from '@saena-io/plugin-sdk';
2
2
  import { RichTextStatic } from '@saena-io/ui/components/rich-text/static';
3
3
  import type { ComponentType } from 'react';
4
4
  import type { ImageValue } from '../field';
5
+ import { singletonMap } from '../singleton';
5
6
 
6
7
  // @saena-io/content/public — the public-site readers a section's React component uses to render field values.
7
8
  // Editor-free (RichTextStatic ships no editor), so the public bundle stays editor-free (ADR-0005/0008). v1
@@ -120,7 +121,7 @@ export function ContentImage({
120
121
  /** A section's public component — receives its hydrated `content`. */
121
122
  export type SectionComponent<C = unknown> = ComponentType<{ content: C }>;
122
123
 
123
- const sectionComponents = new Map<string, SectionComponent>();
124
+ const sectionComponents = singletonMap<string, SectionComponent>('content.sectionComponents');
124
125
 
125
126
  /** Register a section's public component by id (call once at module load, alongside the component). */
126
127
  export function registerSectionComponent<C>(id: string, component: SectionComponent<C>): void {
@@ -0,0 +1,32 @@
1
+ // A process-wide singleton Map, keyed by name on globalThis.
2
+ //
3
+ // Why: content's registries (sections/pages/collections, field types, editors, section components) are
4
+ // declared as module-local `Map`s and shared across this package's entry points (`./define`, `./admin`,
5
+ // `./public`, `./field`). In the monorepo (symlinked source) every importer resolves to one module
6
+ // instance, so a plain `const m = new Map()` is fine. But when @saena-io/content is consumed as a
7
+ // PUBLISHED dependency, a registry written via the package's subpath export (e.g. the app calling
8
+ // `definePage` from `@saena-io/content/define`) and read via the package's INTERNAL relative import
9
+ // (e.g. `buildContentAdmin` reading `allPages` from `../define`) can resolve to two distinct module
10
+ // instances → two separate Maps → the app's registrations are invisible to the admin/public readers
11
+ // (no Content nav, "Screen not found", blank live preview).
12
+ //
13
+ // Anchoring the Map on `globalThis` makes it immune to that duplication: however many times this module
14
+ // is instantiated, every copy reads/writes the same underlying Map. (SSR and the browser have separate
15
+ // globals, which is correct — each side registers and reads within its own runtime.)
16
+
17
+ const NS = '__saenaContentRegistries__';
18
+
19
+ type Store = Record<string, Map<unknown, unknown>>;
20
+
21
+ function store(): Store {
22
+ const g = globalThis as unknown as { [NS]?: Store };
23
+ if (!g[NS]) g[NS] = {};
24
+ return g[NS];
25
+ }
26
+
27
+ /** Get the process-wide Map registered under `key`, creating it once. */
28
+ export function singletonMap<K, V>(key: string): Map<K, V> {
29
+ const s = store();
30
+ if (!s[key]) s[key] = new Map();
31
+ return s[key] as Map<K, V>;
32
+ }