@elytracms/studio 0.0.88 → 0.0.90
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/convex/asset-revalidate.test.ts +35 -42
- package/convex/delivery.ts +8 -11
- package/convex/document-revalidate.test.ts +175 -0
- package/convex/redirect-revalidate.test.ts +36 -36
- package/convex/revalidate.ts +18 -30
- package/convex/triggers.ts +47 -38
- package/convex/webhooks.ts +77 -62
- package/dist/components/account/self-account-view-source.js +2 -1
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +1 -1
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/app-views/default-modules.js +3 -3
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-grid.js +1 -1
- package/dist/components/assets/asset-grid.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.js +3 -2
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/reuse-context.js +2 -2
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +6 -2
- package/dist/components/builder/reuse.js +12 -7
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +0 -6
- package/dist/components/cms/cell-edit.js +5 -23
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +3 -2
- package/dist/components/cms/cms-store.js +23 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +14 -4
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +20 -21
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +3 -2
- package/dist/components/cms/duplicate-document.js +5 -5
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/field-control.js +7 -6
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +13 -6
- package/dist/components/cms/publish-clean.js +14 -6
- package/dist/components/cms/publish-clean.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +3 -2
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -0
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/components/transfer.d.ts +10 -4
- package/dist/components/components/transfer.js +11 -9
- package/dist/components/components/transfer.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +3 -2
- package/dist/components/delivery-health-banner.js +3 -2
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.js +3 -2
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +2 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/ai/apply-content.js +32 -28
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/translate-document.js +11 -7
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +9 -5
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +10 -29
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/vite.d.ts +22 -2
- package/dist/lib/ai/vite.js +12 -3
- package/dist/lib/ai/vite.js.map +1 -1
- package/dist/lib/cms/published-documents.d.ts +14 -3
- package/dist/lib/cms/published-documents.js +12 -3
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +4 -4
- package/dist/lib/cms-views/view-model.js +16 -13
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.js +36 -14
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/delivery/instant-publish.d.ts +0 -3
- package/dist/lib/delivery/instant-publish.js +0 -1
- package/dist/lib/delivery/instant-publish.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/locale/fallback.d.ts +5 -19
- package/dist/lib/locale/fallback.js +18 -39
- package/dist/lib/locale/fallback.js.map +1 -1
- package/dist/lib/locale/locale-store.js +2 -1
- package/dist/lib/locale/locale-store.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +4 -6
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +5 -25
- package/dist/lib/persistence/backend-mode.js +13 -30
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/references/reference-model.d.ts +17 -3
- package/dist/lib/references/reference-model.js +17 -23
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -0
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +0 -7
- package/dist/lib/workspace/index.js +0 -9
- package/dist/lib/workspace/index.js.map +1 -1
- package/package.json +2 -6
- package/dist/lib/webhooks/config.d.ts +0 -15
- package/dist/lib/webhooks/config.js +0 -65
- package/dist/lib/webhooks/config.js.map +0 -1
- package/dist/lib/webhooks/dispatcher.d.ts +0 -70
- package/dist/lib/webhooks/dispatcher.js +0 -61
- package/dist/lib/webhooks/dispatcher.js.map +0 -1
- package/dist/lib/webhooks/index.d.ts +0 -11
- package/dist/lib/webhooks/index.js +0 -10
- package/dist/lib/webhooks/index.js.map +0 -1
- package/dist/lib/webhooks/signing.d.ts +0 -15
- package/dist/lib/webhooks/signing.js +0 -24
- package/dist/lib/webhooks/signing.js.map +0 -1
- package/dist/lib/webhooks/tags.d.ts +0 -25
- package/dist/lib/webhooks/tags.js +0 -289
- package/dist/lib/webhooks/tags.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instant-publish.js","sourceRoot":"","sources":["../../../src/lib/delivery/instant-publish.ts"],"names":[],"mappings":"AAYA,sFAAsF;AACtF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,EAAE,GAAG,EAAE,
|
|
1
|
+
{"version":3,"file":"instant-publish.js","sourceRoot":"","sources":["../../../src/lib/delivery/instant-publish.ts"],"names":[],"mappings":"AAYA,sFAAsF;AACtF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,EAAE,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,4BAA4B,EAAE;IAChE,EAAE,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,0BAA0B,EAAE;CACsB,CAAA;AAEpF,qFAAqF;AACrF,MAAM,UAAU,yBAAyB,CAAC,MAA4B;IACpE,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAC7F,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,0BAA0B,CAAC,MAA4B;IACrE,OAAO,yBAAyB,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,CAAA;AACvD,CAAC","sourcesContent":["/**\n * EC-268: instant-publish (EC-146) configuration health, studio-side. Reads\n * through `operations.delivery.config` (EC-329 — the operations seam, not a\n * raw Convex client) to `PersistenceAdapter.delivery`, reporting whether each\n * delivery env var is set (booleans only — never the values); these helpers\n * turn that into the list the health banner shows. Kept pure + React-free so\n * it is unit-testable and reusable (a future `elytra doctor`, EC-259, can\n * format the same status).\n */\nimport type { DeliveryConfigStatus } from '@elytracms/core/persistence'\nexport type { DeliveryConfigStatus }\n\n/** The pipeline env vars, in setup order, paired with the status key each maps to. */\nexport const INSTANT_PUBLISH_VARS = [\n { key: 'revalidateEndpoint', env: 'CANVAS_REVALIDATE_ENDPOINT' },\n { key: 'revalidateSecret', env: 'CANVAS_REVALIDATE_SECRET' },\n] as const satisfies ReadonlyArray<{ key: keyof DeliveryConfigStatus; env: string }>\n\n/** The env var names that are NOT set, in setup order (empty = fully configured). */\nexport function missingInstantPublishVars(status: DeliveryConfigStatus): string[] {\n return INSTANT_PUBLISH_VARS.filter((entry) => !status[entry.key]).map((entry) => entry.env)\n}\n\n/** True when every instant-publish env var is set. */\nexport function isInstantPublishConfigured(status: DeliveryConfigStatus): boolean {\n return missingInstantPublishVars(status).length === 0\n}\n"]}
|
|
@@ -13,4 +13,4 @@
|
|
|
13
13
|
* (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
|
|
14
14
|
* package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
|
|
15
15
|
*/
|
|
16
|
-
export declare const ENGINE_VERSION = "0.0.
|
|
16
|
+
export declare const ENGINE_VERSION = "0.0.90";
|
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
* (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
|
|
14
14
|
* package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
|
|
15
15
|
*/
|
|
16
|
-
export const ENGINE_VERSION = '0.0.
|
|
16
|
+
export const ENGINE_VERSION = '0.0.90';
|
|
17
17
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../src/lib/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA","sourcesContent":["/**\n * The engine version this studio build was compiled from (EC-552) — the client-side\n * half of the deployed-functions version handshake: the boot gate compares it against\n * what `delivery:engineVersion` answers from the deployment.\n *\n * A MIRROR of `package.json`'s `version`, not the declaration itself: client source\n * cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the\n * root would reshape `dist` and every export path). The parity test beside this file\n * fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,\n * so a release cannot ship with a stale mirror. `pnpm release:bump` writes this\n * alongside the five `package.json` versions (EC-608).\n *\n * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the\n * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)\n */\nexport const ENGINE_VERSION = '0.0.
|
|
1
|
+
{"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../src/lib/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA","sourcesContent":["/**\n * The engine version this studio build was compiled from (EC-552) — the client-side\n * half of the deployed-functions version handshake: the boot gate compares it against\n * what `delivery:engineVersion` answers from the deployment.\n *\n * A MIRROR of `package.json`'s `version`, not the declaration itself: client source\n * cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the\n * root would reshape `dist` and every export path). The parity test beside this file\n * fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,\n * so a release cannot ship with a stale mirror. `pnpm release:bump` writes this\n * alongside the five `package.json` versions (EC-608).\n *\n * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the\n * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)\n */\nexport const ENGINE_VERSION = '0.0.90'\n"]}
|
|
@@ -35,27 +35,21 @@ export declare function resolveDisplayedValue(collection: CollectionDef, doc: Cm
|
|
|
35
35
|
/**
|
|
36
36
|
* True when editing `field` in `locale` would CREATE a new localized variant
|
|
37
37
|
* (rather than update an existing one): the field is localized and the active
|
|
38
|
-
* locale isn't the
|
|
39
|
-
* the "translating from EN"
|
|
38
|
+
* locale isn't the project's default locale (the fallback source — EC-580: the
|
|
39
|
+
* only default there is) and has no variant yet. Drives the "translating from EN"
|
|
40
|
+
* affordance on editors.
|
|
40
41
|
*/
|
|
41
42
|
export declare function editCreatesVariant(collection: CollectionDef, doc: CmsDocument, fieldName: string, locale: Locale, config: LocaleConfig): boolean;
|
|
42
43
|
/** Compact display form of a locale code for chips/badges ("en" → "EN"). */
|
|
43
44
|
export declare function localeChipLabel(locale: Locale): string;
|
|
44
|
-
/**
|
|
45
|
-
* The active-locale value bag: the document's shared `values` overlaid with the
|
|
46
|
-
* active locale's variants (`localized[locale]`). The merged view a surface reads
|
|
47
|
-
* to evaluate conditional-visibility predicates (EC-273) and derive the auto-slug
|
|
48
|
-
* source (EC-315) — deduped between the detail editor and the slug control. Pure.
|
|
49
|
-
*/
|
|
50
|
-
export declare function activeLocaleValues(doc: CmsDocument, locale: Locale): Record<string, unknown>;
|
|
51
45
|
/**
|
|
52
46
|
* The per-locale CONNECTION state of a localized field (EC-318), derived from the
|
|
53
47
|
* document — never stored. `null` when the connected-field affordance does not
|
|
54
|
-
* apply: the field isn't localized, or the requested locale IS the
|
|
48
|
+
* apply: the field isn't localized, or the requested locale IS the project's default
|
|
55
49
|
* locale (there is nothing to follow — the base owns the value).
|
|
56
50
|
*/
|
|
57
51
|
export interface FieldConnection {
|
|
58
|
-
/** The
|
|
52
|
+
/** The base locale — the project's default (EC-580) — whose value a connected field follows. */
|
|
59
53
|
baseLocale: Locale;
|
|
60
54
|
/** True when NO per-locale key exists: the field follows the base value. */
|
|
61
55
|
connected: boolean;
|
|
@@ -66,14 +60,6 @@ export interface FieldConnection {
|
|
|
66
60
|
}
|
|
67
61
|
/** Derive the connected/diverged state of `field` in `locale` (EC-318). */
|
|
68
62
|
export declare function fieldConnection(collection: CollectionDef, doc: CmsDocument, fieldName: string, locale: Locale, config: LocaleConfig): FieldConnection | null;
|
|
69
|
-
/**
|
|
70
|
-
* The present/absent-key write for one field — the FOUNDATION of the connected-
|
|
71
|
-
* field model (EC-176/EC-318). A localized field's value lives under
|
|
72
|
-
* `localized[locale][field]`; writing `undefined` DELETES that key (reconnecting
|
|
73
|
-
* the field to base — the accepted empty-value limitation above). Non-localized
|
|
74
|
-
* fields write the shared `values` bag. Pure — returns a new document.
|
|
75
|
-
*/
|
|
76
|
-
export declare function setFieldValue(collection: CollectionDef, doc: CmsDocument, fieldName: string, locale: Locale, value: unknown): CmsDocument;
|
|
77
63
|
/**
|
|
78
64
|
* "Fill from base" (EC-318) — copy the current base-locale value into the active
|
|
79
65
|
* locale, instantly DISCONNECTING the field with a pre-filled starting point. A
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isFieldLocalized, resolveField, writeField } from '@elytracms/core/cms-core';
|
|
2
2
|
import { isEmptyBlockSequence } from '@elytracms/core/project-graph';
|
|
3
3
|
/** True when the document stores an explicit variant of `field` for `locale`. */
|
|
4
4
|
export function hasLocaleVariant(doc, fieldName, locale) {
|
|
@@ -36,13 +36,14 @@ export function resolveDisplayedValue(collection, doc, fieldName, locale, config
|
|
|
36
36
|
/**
|
|
37
37
|
* True when editing `field` in `locale` would CREATE a new localized variant
|
|
38
38
|
* (rather than update an existing one): the field is localized and the active
|
|
39
|
-
* locale isn't the
|
|
40
|
-
* the "translating from EN"
|
|
39
|
+
* locale isn't the project's default locale (the fallback source — EC-580: the
|
|
40
|
+
* only default there is) and has no variant yet. Drives the "translating from EN"
|
|
41
|
+
* affordance on editors.
|
|
41
42
|
*/
|
|
42
43
|
export function editCreatesVariant(collection, doc, fieldName, locale, config) {
|
|
43
44
|
if (!isFieldLocalized(collection, fieldName))
|
|
44
45
|
return false;
|
|
45
|
-
if (locale ===
|
|
46
|
+
if (locale === config.default)
|
|
46
47
|
return false;
|
|
47
48
|
return !hasLocaleVariant(doc, fieldName, locale);
|
|
48
49
|
}
|
|
@@ -50,15 +51,11 @@ export function editCreatesVariant(collection, doc, fieldName, locale, config) {
|
|
|
50
51
|
export function localeChipLabel(locale) {
|
|
51
52
|
return locale.toUpperCase();
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
export function activeLocaleValues(doc, locale) {
|
|
60
|
-
return { ...doc.values, ...(doc.localized?.[locale] ?? {}) };
|
|
61
|
-
}
|
|
54
|
+
// EC-580: `activeLocaleValues` (`values ∪ localized[L]`, no fallback) is gone. It
|
|
55
|
+
// was one of five answers to "the value of F in L" — the editor's predicates and the
|
|
56
|
+
// auto-slug source read it while the form displayed the fallback, so the publish
|
|
57
|
+
// strip could delete what the editor showed. Every such read is core's
|
|
58
|
+
// `valuesInLocale` now (with `fieldViewLocale` naming whose view judges a field).
|
|
62
59
|
// ───────────────────────────────────────────────────────────────────────────
|
|
63
60
|
// EC-318 — connected fields
|
|
64
61
|
//
|
|
@@ -87,7 +84,7 @@ function isBlankValue(value) {
|
|
|
87
84
|
export function fieldConnection(collection, doc, fieldName, locale, config) {
|
|
88
85
|
if (!isFieldLocalized(collection, fieldName))
|
|
89
86
|
return null;
|
|
90
|
-
const baseLocale =
|
|
87
|
+
const baseLocale = config.default;
|
|
91
88
|
if (locale === baseLocale)
|
|
92
89
|
return null;
|
|
93
90
|
const baseValue = resolveField(collection, doc, fieldName, baseLocale, config).value;
|
|
@@ -98,29 +95,11 @@ export function fieldConnection(collection, doc, fieldName, locale, config) {
|
|
|
98
95
|
canFillFromBase: !isBlankValue(baseValue),
|
|
99
96
|
};
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
* fields write the shared `values` bag. Pure — returns a new document.
|
|
107
|
-
*/
|
|
108
|
-
export function setFieldValue(collection, doc, fieldName, locale, value) {
|
|
109
|
-
if (isFieldLocalized(collection, fieldName)) {
|
|
110
|
-
const perLocale = { ...(doc.localized?.[locale] ?? {}) };
|
|
111
|
-
if (value === undefined)
|
|
112
|
-
delete perLocale[fieldName];
|
|
113
|
-
else
|
|
114
|
-
perLocale[fieldName] = value;
|
|
115
|
-
return { ...doc, localized: { ...doc.localized, [locale]: perLocale } };
|
|
116
|
-
}
|
|
117
|
-
const values = { ...doc.values };
|
|
118
|
-
if (value === undefined)
|
|
119
|
-
delete values[fieldName];
|
|
120
|
-
else
|
|
121
|
-
values[fieldName] = value;
|
|
122
|
-
return { ...doc, values };
|
|
123
|
-
}
|
|
98
|
+
// EC-580: the present/absent-key write for one field — the FOUNDATION of the
|
|
99
|
+
// connected-field model (EC-176/EC-318) — is core's `writeField` now. This module's
|
|
100
|
+
// `setFieldValue` was one of three identical copies (with the table cell's
|
|
101
|
+
// `applyCellValue` and the AI edit's local `writeFieldValue`); every writer reads
|
|
102
|
+
// the one in `@elytracms/core/cms-core`.
|
|
124
103
|
/**
|
|
125
104
|
* "Fill from base" (EC-318) — copy the current base-locale value into the active
|
|
126
105
|
* locale, instantly DISCONNECTING the field with a pre-filled starting point. A
|
|
@@ -130,7 +109,7 @@ export function fillFieldFromBase(collection, doc, fieldName, locale, config) {
|
|
|
130
109
|
const connection = fieldConnection(collection, doc, fieldName, locale, config);
|
|
131
110
|
if (!connection || !connection.canFillFromBase)
|
|
132
111
|
return doc;
|
|
133
|
-
return
|
|
112
|
+
return writeField(collection, doc, fieldName, locale, connection.baseValue);
|
|
134
113
|
}
|
|
135
114
|
/**
|
|
136
115
|
* "Reset to base" / reconnect (EC-318) — delete the active-locale variant so the
|
|
@@ -138,6 +117,6 @@ export function fillFieldFromBase(collection, doc, fieldName, locale, config) {
|
|
|
138
117
|
* a confirm. Pure.
|
|
139
118
|
*/
|
|
140
119
|
export function resetFieldToBase(collection, doc, fieldName, locale) {
|
|
141
|
-
return
|
|
120
|
+
return writeField(collection, doc, fieldName, locale, undefined);
|
|
142
121
|
}
|
|
143
122
|
//# sourceMappingURL=fallback.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/fallback.ts"],"names":[],"mappings":"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","sourcesContent":["import { documentDefaultLocale, isFieldLocalized, resolveField } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { isEmptyBlockSequence } from '@elytracms/core/project-graph'\n\n/**\n * Fallback visibility (EC-176) — the pure \"is this displayed value a\n * fallback?\" decision the CMS table cells, detail-editor fields, and the\n * builder's Content tab all share. One function answers it, so a value is\n * either labeled everywhere or a real translation everywhere — never\n * silently mixed.\n */\n\n/** A displayed value that came from the fallback locale, not the active one. */\nexport interface DisplayedFallback {\n /** The locale the rendered value was actually read from. */\n sourceLocale: Locale\n /** The locale the surface asked for (the ambient active locale). */\n requestedLocale: Locale\n}\n\n/** True when the document stores an explicit variant of `field` for `locale`. */\nexport function hasLocaleVariant(doc: CmsDocument, fieldName: string, locale: Locale): boolean {\n const bag = doc.localized?.[locale]\n return bag !== undefined && fieldName in bag\n}\n\n/**\n * Whether the value a surface displays for `field` in `locale` is served via\n * EC-018 default-locale fallback. `null` means \"not a fallback\": the field is\n * not localized (locale-independent by design), the active locale has its own\n * variant, or there is nothing to display at all (an empty field is empty,\n * not a fallback).\n */\nexport function displayedFieldFallback(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): DisplayedFallback | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n if (hasLocaleVariant(doc, fieldName, locale)) return null\n const resolved = resolveField(collection, doc, fieldName, locale, config)\n if (!resolved.fellBack || resolved.value === undefined) return null\n return { sourceLocale: resolved.sourceLocale, requestedLocale: locale }\n}\n\n/**\n * The value a surface should display for `field` in `locale` (explicit\n * variant, else fallback) together with its fallback marker. Display and\n * label come from the same resolution, so they can never disagree.\n */\nexport function resolveDisplayedValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): { value: unknown; fallback: DisplayedFallback | null } {\n return {\n value: resolveField(collection, doc, fieldName, locale, config).value,\n fallback: displayedFieldFallback(collection, doc, fieldName, locale, config),\n }\n}\n\n/**\n * True when editing `field` in `locale` would CREATE a new localized variant\n * (rather than update an existing one): the field is localized and the active\n * locale isn't the document's fallback source and has no variant yet. Drives\n * the \"translating from EN\" affordance on editors.\n */\nexport function editCreatesVariant(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): boolean {\n if (!isFieldLocalized(collection, fieldName)) return false\n if (locale === documentDefaultLocale(doc, config)) return false\n return !hasLocaleVariant(doc, fieldName, locale)\n}\n\n/** Compact display form of a locale code for chips/badges (\"en\" → \"EN\"). */\nexport function localeChipLabel(locale: Locale): string {\n return locale.toUpperCase()\n}\n\n/**\n * The active-locale value bag: the document's shared `values` overlaid with the\n * active locale's variants (`localized[locale]`). The merged view a surface reads\n * to evaluate conditional-visibility predicates (EC-273) and derive the auto-slug\n * source (EC-315) — deduped between the detail editor and the slug control. Pure.\n */\nexport function activeLocaleValues(doc: CmsDocument, locale: Locale): Record<string, unknown> {\n return { ...doc.values, ...(doc.localized?.[locale] ?? {}) }\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// EC-318 — connected fields\n//\n// The Sanity/Contentful \"connected field\" model, over the SAME present/absent-key\n// storage EC-176 already uses. Connection state is DERIVED, never stored:\n// • connected = NO per-locale key → the field FOLLOWS the base-locale value.\n// • diverged = a per-locale key EXISTS → the field OWNS a locale value.\n// Editing a connected field diverges it (writes the key); \"fill from base\" seeds a\n// divergent copy; \"reset to base\" deletes the key to reconnect.\n//\n// Accepted limitation (a deliberate product decision, not an oversight): an\n// intentionally-EMPTY divergent value is not representable. The built-in controls\n// store a cleared value as `undefined`, which DELETES the key — so clearing a\n// localized field reconnects it to base rather than diverging it to empty.\n// ───────────────────────────────────────────────────────────────────────────\n\n/**\n * The \"no value\" sentinels the built-in controls store (nothing to fill from) —\n * including the empty v2 sequence a cleared canvas writes (EC-575, the same\n * project-graph rule `required` reads). Filling from it would only diverge the\n * locale to an empty value, the state this model deliberately cannot represent.\n */\nfunction isBlankValue(value: unknown): boolean {\n return value === undefined || value === null || value === '' || isEmptyBlockSequence(value)\n}\n\n/**\n * The per-locale CONNECTION state of a localized field (EC-318), derived from the\n * document — never stored. `null` when the connected-field affordance does not\n * apply: the field isn't localized, or the requested locale IS the document's base\n * locale (there is nothing to follow — the base owns the value).\n */\nexport interface FieldConnection {\n /** The document's base/default locale — the value a connected field follows. */\n baseLocale: Locale\n /** True when NO per-locale key exists: the field follows the base value. */\n connected: boolean\n /** The current base-locale value (\"fill from base\"/\"reset\" act against it). */\n baseValue: unknown\n /** True when the base holds a non-empty value there is something to fill from. */\n canFillFromBase: boolean\n}\n\n/** Derive the connected/diverged state of `field` in `locale` (EC-318). */\nexport function fieldConnection(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): FieldConnection | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n const baseLocale = documentDefaultLocale(doc, config)\n if (locale === baseLocale) return null\n const baseValue = resolveField(collection, doc, fieldName, baseLocale, config).value\n return {\n baseLocale,\n connected: !hasLocaleVariant(doc, fieldName, locale),\n baseValue,\n canFillFromBase: !isBlankValue(baseValue),\n }\n}\n\n/**\n * The present/absent-key write for one field — the FOUNDATION of the connected-\n * field model (EC-176/EC-318). A localized field's value lives under\n * `localized[locale][field]`; writing `undefined` DELETES that key (reconnecting\n * the field to base — the accepted empty-value limitation above). Non-localized\n * fields write the shared `values` bag. Pure — returns a new document.\n */\nexport function setFieldValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n value: unknown,\n): CmsDocument {\n if (isFieldLocalized(collection, fieldName)) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\n\n/**\n * \"Fill from base\" (EC-318) — copy the current base-locale value into the active\n * locale, instantly DISCONNECTING the field with a pre-filled starting point. A\n * no-op when there is nothing to copy (blank base value). Pure.\n */\nexport function fillFieldFromBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): CmsDocument {\n const connection = fieldConnection(collection, doc, fieldName, locale, config)\n if (!connection || !connection.canFillFromBase) return doc\n return setFieldValue(collection, doc, fieldName, locale, connection.baseValue)\n}\n\n/**\n * \"Reset to base\" / reconnect (EC-318) — delete the active-locale variant so the\n * field follows the base value again. Discards the translation, so gate it behind\n * a confirm. Pure.\n */\nexport function resetFieldToBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n): CmsDocument {\n return setFieldValue(collection, doc, fieldName, locale, undefined)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../../src/lib/locale/fallback.ts"],"names":[],"mappings":"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","sourcesContent":["import { isFieldLocalized, resolveField, writeField } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { isEmptyBlockSequence } from '@elytracms/core/project-graph'\n\n/**\n * Fallback visibility (EC-176) — the pure \"is this displayed value a\n * fallback?\" decision the CMS table cells, detail-editor fields, and the\n * builder's Content tab all share. One function answers it, so a value is\n * either labeled everywhere or a real translation everywhere — never\n * silently mixed.\n */\n\n/** A displayed value that came from the fallback locale, not the active one. */\nexport interface DisplayedFallback {\n /** The locale the rendered value was actually read from. */\n sourceLocale: Locale\n /** The locale the surface asked for (the ambient active locale). */\n requestedLocale: Locale\n}\n\n/** True when the document stores an explicit variant of `field` for `locale`. */\nexport function hasLocaleVariant(doc: CmsDocument, fieldName: string, locale: Locale): boolean {\n const bag = doc.localized?.[locale]\n return bag !== undefined && fieldName in bag\n}\n\n/**\n * Whether the value a surface displays for `field` in `locale` is served via\n * EC-018 default-locale fallback. `null` means \"not a fallback\": the field is\n * not localized (locale-independent by design), the active locale has its own\n * variant, or there is nothing to display at all (an empty field is empty,\n * not a fallback).\n */\nexport function displayedFieldFallback(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): DisplayedFallback | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n if (hasLocaleVariant(doc, fieldName, locale)) return null\n const resolved = resolveField(collection, doc, fieldName, locale, config)\n if (!resolved.fellBack || resolved.value === undefined) return null\n return { sourceLocale: resolved.sourceLocale, requestedLocale: locale }\n}\n\n/**\n * The value a surface should display for `field` in `locale` (explicit\n * variant, else fallback) together with its fallback marker. Display and\n * label come from the same resolution, so they can never disagree.\n */\nexport function resolveDisplayedValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): { value: unknown; fallback: DisplayedFallback | null } {\n return {\n value: resolveField(collection, doc, fieldName, locale, config).value,\n fallback: displayedFieldFallback(collection, doc, fieldName, locale, config),\n }\n}\n\n/**\n * True when editing `field` in `locale` would CREATE a new localized variant\n * (rather than update an existing one): the field is localized and the active\n * locale isn't the project's default locale (the fallback source — EC-580: the\n * only default there is) and has no variant yet. Drives the \"translating from EN\"\n * affordance on editors.\n */\nexport function editCreatesVariant(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): boolean {\n if (!isFieldLocalized(collection, fieldName)) return false\n if (locale === config.default) return false\n return !hasLocaleVariant(doc, fieldName, locale)\n}\n\n/** Compact display form of a locale code for chips/badges (\"en\" → \"EN\"). */\nexport function localeChipLabel(locale: Locale): string {\n return locale.toUpperCase()\n}\n\n// EC-580: `activeLocaleValues` (`values ∪ localized[L]`, no fallback) is gone. It\n// was one of five answers to \"the value of F in L\" — the editor's predicates and the\n// auto-slug source read it while the form displayed the fallback, so the publish\n// strip could delete what the editor showed. Every such read is core's\n// `valuesInLocale` now (with `fieldViewLocale` naming whose view judges a field).\n\n// ───────────────────────────────────────────────────────────────────────────\n// EC-318 — connected fields\n//\n// The Sanity/Contentful \"connected field\" model, over the SAME present/absent-key\n// storage EC-176 already uses. Connection state is DERIVED, never stored:\n// • connected = NO per-locale key → the field FOLLOWS the base-locale value.\n// • diverged = a per-locale key EXISTS → the field OWNS a locale value.\n// Editing a connected field diverges it (writes the key); \"fill from base\" seeds a\n// divergent copy; \"reset to base\" deletes the key to reconnect.\n//\n// Accepted limitation (a deliberate product decision, not an oversight): an\n// intentionally-EMPTY divergent value is not representable. The built-in controls\n// store a cleared value as `undefined`, which DELETES the key — so clearing a\n// localized field reconnects it to base rather than diverging it to empty.\n// ───────────────────────────────────────────────────────────────────────────\n\n/**\n * The \"no value\" sentinels the built-in controls store (nothing to fill from) —\n * including the empty v2 sequence a cleared canvas writes (EC-575, the same\n * project-graph rule `required` reads). Filling from it would only diverge the\n * locale to an empty value, the state this model deliberately cannot represent.\n */\nfunction isBlankValue(value: unknown): boolean {\n return value === undefined || value === null || value === '' || isEmptyBlockSequence(value)\n}\n\n/**\n * The per-locale CONNECTION state of a localized field (EC-318), derived from the\n * document — never stored. `null` when the connected-field affordance does not\n * apply: the field isn't localized, or the requested locale IS the project's default\n * locale (there is nothing to follow — the base owns the value).\n */\nexport interface FieldConnection {\n /** The base locale — the project's default (EC-580) — whose value a connected field follows. */\n baseLocale: Locale\n /** True when NO per-locale key exists: the field follows the base value. */\n connected: boolean\n /** The current base-locale value (\"fill from base\"/\"reset\" act against it). */\n baseValue: unknown\n /** True when the base holds a non-empty value there is something to fill from. */\n canFillFromBase: boolean\n}\n\n/** Derive the connected/diverged state of `field` in `locale` (EC-318). */\nexport function fieldConnection(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): FieldConnection | null {\n if (!isFieldLocalized(collection, fieldName)) return null\n const baseLocale = config.default\n if (locale === baseLocale) return null\n const baseValue = resolveField(collection, doc, fieldName, baseLocale, config).value\n return {\n baseLocale,\n connected: !hasLocaleVariant(doc, fieldName, locale),\n baseValue,\n canFillFromBase: !isBlankValue(baseValue),\n }\n}\n\n// EC-580: the present/absent-key write for one field — the FOUNDATION of the\n// connected-field model (EC-176/EC-318) — is core's `writeField` now. This module's\n// `setFieldValue` was one of three identical copies (with the table cell's\n// `applyCellValue` and the AI edit's local `writeFieldValue`); every writer reads\n// the one in `@elytracms/core/cms-core`.\n\n/**\n * \"Fill from base\" (EC-318) — copy the current base-locale value into the active\n * locale, instantly DISCONNECTING the field with a pre-filled starting point. A\n * no-op when there is nothing to copy (blank base value). Pure.\n */\nexport function fillFieldFromBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n config: LocaleConfig,\n): CmsDocument {\n const connection = fieldConnection(collection, doc, fieldName, locale, config)\n if (!connection || !connection.canFillFromBase) return doc\n return writeField(collection, doc, fieldName, locale, connection.baseValue)\n}\n\n/**\n * \"Reset to base\" / reconnect (EC-318) — delete the active-locale variant so the\n * field follows the base value again. Discards the translation, so gate it behind\n * a confirm. Pure.\n */\nexport function resetFieldToBase(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n): CmsDocument {\n return writeField(collection, doc, fieldName, locale, undefined)\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isKnownLocale } from '@elytracms/core/cms-core';
|
|
2
|
+
import { parseLocaleConfig } from '@elytracms/core/cms-core';
|
|
2
3
|
/** The pre-config state: no project locales have been installed yet. */
|
|
3
4
|
const NOT_LOADED = { config: null, active: null };
|
|
4
5
|
let state = NOT_LOADED;
|
|
@@ -54,7 +55,7 @@ export function setStudioLocale(locale) {
|
|
|
54
55
|
* set" is exactly the drift the one gate exists to prevent.
|
|
55
56
|
*/
|
|
56
57
|
export function configureStudioLocales(config) {
|
|
57
|
-
const normalized = { default: config.default, locales: [...config.locales] };
|
|
58
|
+
const normalized = parseLocaleConfig({ default: config.default, locales: [...config.locales] });
|
|
58
59
|
const current = state.config;
|
|
59
60
|
const sameConfig = current !== null &&
|
|
60
61
|
normalized.default === current.default &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"locale-store.js","sourceRoot":"","sources":["../../../src/lib/locale/locale-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAExD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAoC5D,wEAAwE;AACxE,MAAM,UAAU,GAAsB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;AAEpE,IAAI,KAAK,GAAsB,UAAU,CAAA;AAEzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;AAEvC,SAAS,IAAI;IACX,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAA;AAC9C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,oBAAoB;IAClC,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,KAAK,CAAC,MAAM,CAAA;AACrB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAM;IACjC,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC;QAAE,OAAM;IAC3E,KAAK,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IAChD,IAAI,EAAE,CAAA;AACR,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAoB;IACzD,MAAM,UAAU,GAAiB,iBAAiB,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC7G,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAA;IAC5B,MAAM,UAAU,GACd,OAAO,KAAK,IAAI;QAChB,UAAU,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO;QACtC,UAAU,CAAC,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,MAAM;QACpD,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IACxE,MAAM,MAAM,GACV,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC;QAC9D,CAAC,CAAC,KAAK,CAAC,MAAM;QACd,CAAC,CAAC,UAAU,CAAC,OAAO,CAAA;IACxB,IAAI,UAAU,IAAI,MAAM,KAAK,KAAK,CAAC,MAAM;QAAE,OAAM;IACjD,KAAK,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IACtC,IAAI,EAAE,CAAA;AACR,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,QAAoB;IACxD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB;IAC/B,KAAK,GAAG,UAAU,CAAA;IAClB,IAAI,EAAE,CAAA;AACR,CAAC","sourcesContent":["import { isKnownLocale } from '@elytracms/core/cms-core'\nimport type { Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { parseLocaleConfig } from '@elytracms/core/cms-core'\n\n/**\n * The studio's ambient locale (EC-176) — framework-free core.\n *\n * One studio-level active locale, set once via the locale switcher, that the\n * builder canvas preview, the CMS table, and the detail editor all follow.\n * Module-level (not React context) on purpose: the playground's content\n * client (`lib/data/collection-content`) reads it at query time without any\n * provider plumbing, and React surfaces subscribe through\n * `useStudioLocale` (`useSyncExternalStore`).\n *\n * Deliberately NOT part of `lib/data/data-store` — the locale is studio\n * state, not a data source.\n *\n * EC-479/EC-483 — **before a project's locale config is installed, this store\n * holds `null`, not a locale.** It used to open on the engine's `en/de`\n * `sampleLocaleConfig` fixture, which is the worst fallback in the family: the\n * default locale is the literal STORAGE KEY for every localized field value\n * (`documents.ts:88`), so a fabricated one writes a German project's content\n * under `en` and correcting it later is a data migration, not a config edit. The\n * locale is a deliberate choice at project creation (EC-483) and reaches the\n * studio through {@link configureStudioLocales} from the project's own config —\n * so \"not loaded\" is an honest state that surfaces, never an invented one that\n * silently becomes a key.\n */\n\n/**\n * `{ config, active }` once a project's locales are installed; `{ null, null }`\n * before that. The two fields move together by construction — there is no state\n * with a config but no active locale, or an active locale nobody declared.\n */\nexport type StudioLocaleState =\n | { readonly config: LocaleConfig; readonly active: Locale }\n | { readonly config: null; readonly active: null }\n\n/** The pre-config state: no project locales have been installed yet. */\nconst NOT_LOADED: StudioLocaleState = { config: null, active: null }\n\nlet state: StudioLocaleState = NOT_LOADED\n\nconst listeners = new Set<() => void>()\n\nfunction emit(): void {\n for (const listener of listeners) listener()\n}\n\n/** Snapshot of `{ config, active }` — stable identity until the next change. */\nexport function getStudioLocaleState(): StudioLocaleState {\n return state\n}\n\n/**\n * The active studio locale every surface renders in — `null` until a project's\n * locale config is installed. A caller that genuinely needs a locale reads the\n * PROJECT's own default (`CmsStore.localeConfig.default`, the content session's\n * config); it must never substitute one of its own.\n */\nexport function getStudioLocale(): Locale | null {\n return state.active\n}\n\n/**\n * The project locale config driving the switcher options — `null` until one is\n * installed (the studio has no locales of its own to offer).\n */\nexport function getStudioLocaleConfig(): LocaleConfig | null {\n return state.config\n}\n\n/**\n * Switch the ambient locale. Unknown locales are ignored (the switcher only\n * offers configured ones; programmatic callers get a no-op, never a crash), and\n * so is any switch attempted before a config is installed — there is nothing to\n * validate the locale against.\n */\nexport function setStudioLocale(locale: Locale): void {\n if (state.config === null) return\n if (locale === state.active || !isKnownLocale(state.config, locale)) return\n state = { config: state.config, active: locale }\n emit()\n}\n\n/**\n * Install the project's locale config (when a CMS session mounts). The active\n * locale is kept when it is still configured, otherwise it snaps back to the new\n * default.\n *\n * The config arrives valid by construction: every caller hands over a\n * config-derived `LocaleConfig` that the workspace gate has already checked\n * (`default-locale-not-in-locales` is an error there), so nothing is repaired\n * here. The old `createLocaleConfig` repair existed for starter-kit locales, a\n * rung EC-574 removed — a second, silent declaration of \"the default is in the\n * set\" is exactly the drift the one gate exists to prevent.\n */\nexport function configureStudioLocales(config: LocaleConfig): void {\n const normalized: LocaleConfig = parseLocaleConfig({ default: config.default, locales: [...config.locales] })\n const current = state.config\n const sameConfig =\n current !== null &&\n normalized.default === current.default &&\n normalized.locales.length === current.locales.length &&\n normalized.locales.every((locale, i) => locale === current.locales[i])\n const active =\n state.active !== null && isKnownLocale(normalized, state.active)\n ? state.active\n : normalized.default\n if (sameConfig && active === state.active) return\n state = { config: normalized, active }\n emit()\n}\n\n/** Subscribe to locale/config changes. Returns the unsubscribe function. */\nexport function subscribeStudioLocale(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Reset to the pre-config \"not loaded\" state (tests only — module state is\n * shared). Never re-seeds a fixture config: a test that queries localized content\n * must say which locale it means, exactly like a project does.\n */\nexport function resetStudioLocale(): void {\n state = NOT_LOADED\n emit()\n}\n"]}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { activeDeploymentKey, selectPersistenceAdapter } from '../persistence/index.js';
|
|
2
2
|
import { attachSessionExpiryReporting } from '../session-expiry.js';
|
|
3
|
-
import { attachConfiguredRevalidationWebhooks } from '../webhooks/index.js';
|
|
4
3
|
import { sessionValidateDocument } from '../cms/document-gate.js';
|
|
5
4
|
import { attachReferenceIndex, createChangesets, createOperations, createRoleAuthorization, } from './operations-package.js';
|
|
6
5
|
/**
|
|
@@ -45,11 +44,10 @@ export function createStudioOperations(adapter, options = {}) {
|
|
|
45
44
|
validateDocument: sessionValidateDocument,
|
|
46
45
|
...authorized,
|
|
47
46
|
});
|
|
48
|
-
// EC-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
attachConfiguredRevalidationWebhooks(operations.events);
|
|
47
|
+
// EC-579: no revalidation emitter attaches here. Host revalidation is a property
|
|
48
|
+
// of the Convex write chokepoint (`convex/triggers.ts`), the one emitter — it
|
|
49
|
+
// catches every writer, keeps the signing secret server-side, and the playground's
|
|
50
|
+
// in-browser adapter is read by no host, so there is nothing to emit there.
|
|
53
51
|
// EC-403: session-expiry detection rides the SAME lifecycle bus — every failed
|
|
54
52
|
// command/query/changeset operation is checked against the session-expired
|
|
55
53
|
// predicate here, so expiry raises the ONE app-level blocking re-auth state no
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,oCAAoC,EAAE,MAAM,aAAa,CAAA;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAC9D,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,sBAAsB,CAAA;AAgD7B;;;;GAIG;AACH,SAAS,4BAA4B;IACnC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC,EAAE,CAAA;AACpD,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,sBAAsB,CACpC,OAA2B,EAC3B,UAAyC,EAAE;IAE3C,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK;QAC9B,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,SAAS,EAAE,uBAAuB,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;QAC/E,CAAC,CAAC,EAAE,CAAA;IACN,6EAA6E;IAC7E,4EAA4E;IAC5E,2EAA2E;IAC3E,gFAAgF;IAChF,gFAAgF;IAChF,8EAA8E;IAC9E,6EAA6E;IAC7E,oDAAoD;IACpD,MAAM,UAAU,GAAG,gBAAgB,CAAC;QAClC,OAAO;QACP,gBAAgB,EAAE,uBAAuB;QACzC,GAAG,UAAU;KACd,CAAC,CAAA;IACF,mFAAmF;IACnF,gFAAgF;IAChF,gFAAgF;IAChF,kFAAkF;IAClF,+EAA+E;IAC/E,0EAA0E;IAC1E,MAAM,UAAU,GAAG,OAAO,CAAC,uBAAuB;QAChD,CAAC,CAAC,4BAA4B,EAAE;QAChC,CAAC,CAAC,oBAAoB,CAAC,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpD,MAAM,UAAU,GAAG,gBAAgB,CAAC;QAClC,OAAO;QACP,MAAM,EAAE,UAAU,CAAC,MAAM;QACzB,iFAAiF;QACjF,gBAAgB,EAAE,uBAAuB;QACzC,GAAG,UAAU;KACd,CAAC,CAAA;IACF,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,8BAA8B;IAC9B,oCAAoC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;IACvD,+EAA+E;IAC/E,2EAA2E;IAC3E,+EAA+E;IAC/E,6EAA6E;IAC7E,mCAAmC;IACnC,4BAA4B,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;IAC/C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,CAAA;AACxD,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,2EAA2E;AAC3E,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAA;AACpE,MAAM,cAAc,GAAG,IAAI,GAAG,EAAqC,CAAA;AACnE,IAAI,YAAY,GAAqB,IAAI,CAAA;AAEzC;;;;;GAKG;AACH,SAAS,aAAa;IACpB,IAAI,CAAC;QACH,OAAO,mBAAmB,EAAE,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,kBAAkB,CAAA;IAC3B,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,IAAI,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,wBAAwB,EAAE,CAAA;QACpC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;IACjC,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,GAAG,aAAa,EAAE,CAAA;IAC3B,IAAI,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,KAAK,GAAG,YAAY,CAAA;QAC1B,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAC1C,sBAAsB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACxD,CAAA;QACD,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAuB;IACtD,IAAI,YAAY,EAAE,EAAE,KAAK,KAAK,EAAE,EAAE;QAAE,OAAM,CAAC,8CAA8C;IACzF,YAAY,GAAG,KAAK,CAAA;IACpB,cAAc,CAAC,KAAK,EAAE,CAAA;AACxB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,2BAA2B,CAAC,IAA6B;IACvE,IAAI,IAAI,EAAE,CAAC;QACT,cAAc,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;QAC1D,OAAM;IACR,CAAC;IACD,cAAc,CAAC,KAAK,EAAE,CAAA;IACtB,aAAa,CAAC,KAAK,EAAE,CAAA;IACrB,YAAY,GAAG,IAAI,CAAA;AACrB,CAAC","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { attachConfiguredRevalidationWebhooks } from '../webhooks'\nimport { sessionValidateDocument } from '../cms/document-gate'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n // EC-497: the publish gate validates with the CMS session's canonical option\n // bag — the same one the detail editor, bulk publish, cell edit, import and\n // every AI path use — instead of deciding its own (weaker, mode-dependent)\n // strength off the adapter-served schema record. The seam reads a validator the\n // mounted session published; with no session it answers with ONE blocking issue\n // (EC-546) — a publish issued outside a mounted CMS session is refused, never\n // waved through on weaker rules. The validator is required by the operations\n // layer, so this bundle cannot be built without it.\n const operations = createOperations({\n adapter,\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({\n adapter,\n events: operations.events,\n // EC-497: a changeset-driven publish is gated identically — same seam, same bag.\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-146 instant publish: when a host revalidate endpoint is configured via\n // env, every successful publish-shaped command (document upsert/delete/\n // setState, graph publish/unpublish) fires a signed revalidation webhook.\n // No env (dev/tests) → no-op.\n attachConfiguredRevalidationWebhooks(operations.events)\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"studio-operations.js","sourceRoot":"","sources":["../../../src/lib/operations/studio-operations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PersistenceAdapter } from '@elytracms/core/persistence'\nimport { activeDeploymentKey, selectPersistenceAdapter } from '../persistence'\nimport { attachSessionExpiryReporting } from '../session-expiry'\nimport { sessionValidateDocument } from '../cms/document-gate'\nimport {\n attachReferenceIndex,\n createChangesets,\n createOperations,\n createRoleAuthorization,\n} from './operations-package'\nimport type {\n ChangesetOperations,\n OperationsClient,\n Principal,\n ReferenceIndexAttachment,\n} from './operations-package'\n\n/**\n * The studio's single operations bootstrap (EC-141, vision AD-7).\n *\n * One `OperationsClient` + one `ChangesetOperations` instance, built over the\n * selected persistence adapter, shared by every store and surface in the app.\n * The UI never talks to a repository for writes — every mutation is a typed\n * operation command (validate → authorize → mutate → event), so the studio\n * dogfoods the same seam plugins, AI changesets, and the CLI use.\n *\n * The EC-155 reference index is attached to the shared event bus here, so any\n * document/graph/publishing command issued anywhere in the studio keeps the\n * index maintained.\n */\nexport interface StudioOperations {\n readonly operations: OperationsClient\n readonly changesets: ChangesetOperations\n /**\n * The raw adapter, exposed for *transport and read paths only*: blob byte\n * upload/download (`adapter.blobs`) and existing read stores. Studio code\n * must not call repository write methods on it — writes go through\n * `operations`.\n */\n readonly adapter: PersistenceAdapter\n /** EC-155 maintained-on-write index attachment (flushable in tests). */\n readonly references: ReferenceIndexAttachment\n}\n\nexport interface CreateStudioOperationsOptions {\n /**\n * The signed-in principal (EC-150). When present, the EC-150 role hook\n * (`createRoleAuthorization` over `adapter.members`) becomes the\n * authorization chokepoint for every command and query — viewers get\n * structured denials on mutations, editors on administration. Without an\n * actor the client runs pass-through (seeds, fixtures, tests that exercise\n * domain logic rather than permissions); the route guard keeps\n * unauthenticated users away from every operation surface.\n */\n actor?: Principal\n}\n\n/**\n * A no-op {@link ReferenceIndexAttachment} for self-maintaining adapters (EC-277):\n * nothing is subscribed, so `detach` and `flush` are trivially satisfied (the index\n * is kept current in the adapter's write path, not by a queued subscriber).\n */\nfunction noopReferenceIndexAttachment(): ReferenceIndexAttachment {\n return { detach: () => {}, flush: async () => {} }\n}\n\n/** Build a studio operations bundle over an adapter (exported for tests). */\nexport function createStudioOperations(\n adapter: PersistenceAdapter,\n options: CreateStudioOperationsOptions = {},\n): StudioOperations {\n const authorized = options.actor\n ? { actor: options.actor, authorize: createRoleAuthorization(adapter.members) }\n : {}\n // EC-497: the publish gate validates with the CMS session's canonical option\n // bag — the same one the detail editor, bulk publish, cell edit, import and\n // every AI path use — instead of deciding its own (weaker, mode-dependent)\n // strength off the adapter-served schema record. The seam reads a validator the\n // mounted session published; with no session it answers with ONE blocking issue\n // (EC-546) — a publish issued outside a mounted CMS session is refused, never\n // waved through on weaker rules. The validator is required by the operations\n // layer, so this bundle cannot be built without it.\n const operations = createOperations({\n adapter,\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-277: when the adapter maintains the index in its own write path (AD-6 \"at the\n // source\" — the Convex provider does this server-side, so a raw `convex run` is\n // caught too), the client-side subscriber would only double the work. Attach it\n // ONLY for providers that don't self-maintain. Where it does run, extraction is a\n // schema-free, manifest-free walk over self-describing content, so it needs no\n // registry (compositions self-typed their relation/asset props on write).\n const references = adapter.maintainsReferenceIndex\n ? noopReferenceIndexAttachment()\n : attachReferenceIndex(operations.events, adapter)\n const changesets = createChangesets({\n adapter,\n events: operations.events,\n // EC-497: a changeset-driven publish is gated identically — same seam, same bag.\n validateDocument: sessionValidateDocument,\n ...authorized,\n })\n // EC-579: no revalidation emitter attaches here. Host revalidation is a property\n // of the Convex write chokepoint (`convex/triggers.ts`), the one emitter — it\n // catches every writer, keeps the signing secret server-side, and the playground's\n // in-browser adapter is read by no host, so there is nothing to emit there.\n // EC-403: session-expiry detection rides the SAME lifecycle bus — every failed\n // command/query/changeset operation is checked against the session-expired\n // predicate here, so expiry raises the ONE app-level blocking re-auth state no\n // matter which surface issued the operation (per-surface banners can't cover\n // surfaces that render no banner).\n attachSessionExpiryReporting(operations.events)\n return { operations, changesets, adapter, references }\n}\n\n// One adapter + one operations bundle per backend (EC-179): keyed by the active\n// deployment so switching to a project on another Convex deployment selects (or\n// lazily builds) that backend's client, rather than reusing the first one.\nconst adaptersByKey = new Map<string, Promise<PersistenceAdapter>>()\nconst instancesByKey = new Map<string, Promise<StudioOperations>>()\nlet currentActor: Principal | null = null\n\n/**\n * The active backend key, tolerant of an unconfigured env: the real app never\n * reaches `getStudioOperations` while misconfigured (the `DeploymentSetupGate` renders a\n * boot error first), but tests inject a bundle directly via the seam below — so\n * a missing backend resolves to one stable key rather than throwing.\n */\nfunction deploymentKey(): string {\n try {\n return activeDeploymentKey()\n } catch {\n return '__unconfigured__'\n }\n}\n\nfunction getAdapter(key: string): Promise<PersistenceAdapter> {\n let adapter = adaptersByKey.get(key)\n if (!adapter) {\n adapter = selectPersistenceAdapter()\n adaptersByKey.set(key, adapter)\n }\n return adapter\n}\n\n/**\n * The shared studio operations client for the active backend (lazily\n * bootstrapped, cached per deployment). In-memory mode this owns the one seeded\n * adapter the whole session mutates; against Convex it owns one client per\n * deployment — the active project (EC-179) selects which.\n */\nexport function getStudioOperations(): Promise<StudioOperations> {\n const key = deploymentKey()\n let instance = instancesByKey.get(key)\n if (!instance) {\n const actor = currentActor\n instance = getAdapter(key).then((adapter) =>\n createStudioOperations(adapter, actor ? { actor } : {}),\n )\n instancesByKey.set(key, instance)\n }\n return instance\n}\n\n/**\n * Bind (or clear) the session principal (EC-150). Called by the auth context\n * on sign-in/sign-out. Rebuilds the operations clients over the *same* adapters\n * — the persisted state (and, in-memory, the whole session's edits) survives;\n * only the authorization identity changes. All cached backends are rebuilt so a\n * later switch picks up the new actor too.\n */\nexport function setStudioSession(actor: Principal | null): void {\n if (currentActor?.id === actor?.id) return // same user (or both null) — keep the clients\n currentActor = actor\n instancesByKey.clear()\n}\n\n/** Test seam: replace (or clear) the singleton for the active backend. */\nexport function setStudioOperationsForTests(next: StudioOperations | null): void {\n if (next) {\n instancesByKey.set(deploymentKey(), Promise.resolve(next))\n return\n }\n instancesByKey.clear()\n adaptersByKey.clear()\n currentActor = null\n}\n"]}
|
|
@@ -1,31 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
* Studio backend mode resolution (EC-153). Convex is the **default and primary**
|
|
3
|
-
* persistence backend; the deterministic in-memory adapter is demoted to an
|
|
4
|
-
* explicit, labeled **playground** mode and is never an automatic fallback:
|
|
5
|
-
*
|
|
6
|
-
* - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).
|
|
7
|
-
* - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,
|
|
8
|
-
* local + ephemeral). The flag wins over a configured URL — explicit beats
|
|
9
|
-
* implicit — so a checked-out `.env.local` can be flipped without deleting it.
|
|
10
|
-
* - Neither → **misconfigured**: the studio renders a deterministic boot-error
|
|
11
|
-
* screen (see `DeploymentSetupGate`) telling the developer exactly what to set.
|
|
12
|
-
* Likewise, a Convex backend that is unreachable at runtime is a visible
|
|
13
|
-
* error state — the studio never silently swaps in in-memory storage.
|
|
14
|
-
*/
|
|
1
|
+
import type { BackendMode } from '@elytracms/core/export-sync/deployment-ready';
|
|
15
2
|
export declare const MODE_ENV_VAR = "VITE_ELYTRA_MODE";
|
|
16
3
|
export declare const PLAYGROUND_MODE = "playground";
|
|
17
4
|
export declare const CONVEX_URL_ENV_VAR = "VITE_CONVEX_URL";
|
|
18
5
|
export type StudioBackendMode = 'convex' | 'playground';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
url: string;
|
|
22
|
-
} | {
|
|
23
|
-
kind: 'playground';
|
|
24
|
-
} | {
|
|
25
|
-
kind: 'misconfigured';
|
|
26
|
-
reason: 'missing-convex-url' | 'unknown-mode';
|
|
27
|
-
detail: string;
|
|
28
|
-
};
|
|
6
|
+
/** The backend verdict — core's {@link BackendMode}, under the studio's historical name. */
|
|
7
|
+
export type StudioBackendConfig = BackendMode;
|
|
29
8
|
/** The studio's env surface: Vite client env first, Node/SSR env second. */
|
|
30
9
|
export type EnvRecord = Record<string, string | undefined>;
|
|
31
10
|
/**
|
|
@@ -91,7 +70,8 @@ export type StudioEnvAnomaly =
|
|
|
91
70
|
export declare function detectStudioEnvAnomaly(provided: EnvRecord | null, engineVisible: EnvRecord | null, inBrowser: boolean): StudioEnvAnomaly | null;
|
|
92
71
|
/**
|
|
93
72
|
* Pure mode decision — testable with a plain env record. The app calls it with
|
|
94
|
-
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.
|
|
73
|
+
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools. The rule
|
|
74
|
+
* itself is core's `parseBackendMode` (EC-603) — the same one the CLI applies.
|
|
95
75
|
*/
|
|
96
76
|
export declare function resolveBackendConfig(env?: EnvRecord): StudioBackendConfig;
|
|
97
77
|
/** True when the studio runs in the explicit, labeled playground mode. */
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
* screen (see `DeploymentSetupGate`) telling the developer exactly what to set.
|
|
12
12
|
* Likewise, a Convex backend that is unreachable at runtime is a visible
|
|
13
13
|
* error state — the studio never silently swaps in in-memory storage.
|
|
14
|
+
*
|
|
15
|
+
* EC-603: the RULE above lives in ONE place — core's `parseBackendMode`, which the
|
|
16
|
+
* `elytra` CLI runs over the same env bytes. This module keeps the studio's env SOURCE
|
|
17
|
+
* (the mount-provided env, {@link studioEnv}) and hands it to that parser.
|
|
14
18
|
*/
|
|
15
|
-
|
|
16
|
-
export const
|
|
17
|
-
export const
|
|
19
|
+
import { BACKEND_MODE_ENV_VAR, BACKEND_URL_ENV_VAR, PLAYGROUND_MODE as CORE_PLAYGROUND_MODE, parseBackendMode, readEnvValue, } from '@elytracms/core/export-sync/deployment-ready';
|
|
20
|
+
export const MODE_ENV_VAR = BACKEND_MODE_ENV_VAR;
|
|
21
|
+
export const PLAYGROUND_MODE = CORE_PLAYGROUND_MODE;
|
|
22
|
+
export const CONVEX_URL_ENV_VAR = BACKEND_URL_ENV_VAR;
|
|
18
23
|
/**
|
|
19
24
|
* The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.
|
|
20
25
|
* `null` is a meaningful signal (EC-357): every module Vite transforms live gets an
|
|
@@ -35,10 +40,8 @@ function viteEnv() {
|
|
|
35
40
|
function nodeEnv() {
|
|
36
41
|
return typeof process !== 'undefined' ? (process.env ?? {}) : {};
|
|
37
42
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return value && value.trim() !== '' ? value.trim() : undefined;
|
|
41
|
-
}
|
|
43
|
+
/** Trimmed, blank-is-absent — the parser's own read, so every studio env key reads alike. */
|
|
44
|
+
const readEnv = readEnvValue;
|
|
42
45
|
let envOverride = null;
|
|
43
46
|
/**
|
|
44
47
|
* Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the
|
|
@@ -165,31 +168,11 @@ function maybeWarnEngineEnvStale() {
|
|
|
165
168
|
}
|
|
166
169
|
/**
|
|
167
170
|
* Pure mode decision — testable with a plain env record. The app calls it with
|
|
168
|
-
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.
|
|
171
|
+
* {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools. The rule
|
|
172
|
+
* itself is core's `parseBackendMode` (EC-603) — the same one the CLI applies.
|
|
169
173
|
*/
|
|
170
174
|
export function resolveBackendConfig(env = studioEnv()) {
|
|
171
|
-
|
|
172
|
-
if (mode === PLAYGROUND_MODE)
|
|
173
|
-
return { kind: 'playground' };
|
|
174
|
-
if (mode !== undefined) {
|
|
175
|
-
return {
|
|
176
|
-
kind: 'misconfigured',
|
|
177
|
-
reason: 'unknown-mode',
|
|
178
|
-
detail: `${MODE_ENV_VAR}="${mode}" is not a known mode. The only supported value is "${PLAYGROUND_MODE}"; unset it to run against Convex.`,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const url = readEnv(env, CONVEX_URL_ENV_VAR) ?? readEnv(env, 'CONVEX_URL');
|
|
182
|
-
if (url)
|
|
183
|
-
return { kind: 'convex', url };
|
|
184
|
-
return {
|
|
185
|
-
kind: 'misconfigured',
|
|
186
|
-
reason: 'missing-convex-url',
|
|
187
|
-
// EC-357: name BOTH causes — the var may be genuinely unset, or set but never
|
|
188
|
-
// reaching the client at runtime (Vite dep-optimized engine code; see
|
|
189
|
-
// `provideStudioEnv`). The paired fix text (deployment-ready `FIX.backendUrl`)
|
|
190
|
-
// carries the full remedy for the second cause.
|
|
191
|
-
detail: `No persistence backend is configured — ${CONVEX_URL_ENV_VAR} is unset or is not reaching the client at runtime. Set it to your Convex deployment URL (see docs/backend-setup.md), or opt into the ephemeral local playground with ${MODE_ENV_VAR}=${PLAYGROUND_MODE}.`,
|
|
192
|
-
};
|
|
175
|
+
return parseBackendMode(env);
|
|
193
176
|
}
|
|
194
177
|
/** True when the studio runs in the explicit, labeled playground mode. */
|
|
195
178
|
export function isPlaygroundMode() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend-mode.js","sourceRoot":"","sources":["../../../src/lib/persistence/backend-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Studio backend mode resolution (EC-153). Convex is the **default and primary**\n * persistence backend; the deterministic in-memory adapter is demoted to an\n * explicit, labeled **playground** mode and is never an automatic fallback:\n *\n * - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).\n * - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,\n * local + ephemeral). The flag wins over a configured URL — explicit beats\n * implicit — so a checked-out `.env.local` can be flipped without deleting it.\n * - Neither → **misconfigured**: the studio renders a deterministic boot-error\n * screen (see `DeploymentSetupGate`) telling the developer exactly what to set.\n * Likewise, a Convex backend that is unreachable at runtime is a visible\n * error state — the studio never silently swaps in in-memory storage.\n */\n\nexport const MODE_ENV_VAR = 'VITE_ELYTRA_MODE'\nexport const PLAYGROUND_MODE = 'playground'\nexport const CONVEX_URL_ENV_VAR = 'VITE_CONVEX_URL'\n\nexport type StudioBackendMode = 'convex' | 'playground'\n\nexport type StudioBackendConfig =\n | { kind: 'convex'; url: string }\n | { kind: 'playground' }\n | { kind: 'misconfigured'; reason: 'missing-convex-url' | 'unknown-mode'; detail: string }\n\n/** The studio's env surface: Vite client env first, Node/SSR env second. */\nexport type EnvRecord = Record<string, string | undefined>\n\n/**\n * The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.\n * `null` is a meaningful signal (EC-357): every module Vite transforms live gets an\n * `import.meta.env`, so its complete absence in a browser means this engine module\n * was served from Vite's dep-optimizer cache — where Vite ≥8 no longer inlines\n * custom `VITE_*` values (and Vite 7 froze them at optimize time). Engine code must\n * therefore never trust this view when a mount-provided env exists — see\n * {@link provideStudioEnv}.\n */\nfunction engineVisibleEnv(): EnvRecord | null {\n if (typeof import.meta === 'undefined') return null\n return (import.meta as { env?: EnvRecord }).env ?? null\n}\n\nfunction viteEnv(): EnvRecord {\n return engineVisibleEnv() ?? {}\n}\n\nfunction nodeEnv(): EnvRecord {\n return typeof process !== 'undefined' ? (process.env ?? {}) : {}\n}\n\nfunction readEnv(env: EnvRecord, key: string): string | undefined {\n const value = env[key]\n return value && value.trim() !== '' ? value.trim() : undefined\n}\n\nlet envOverride: EnvRecord | null = null\n\n/**\n * Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the\n * `import.meta.env` snapshot Vitest hands this module, so suites that exercise\n * `selectPersistenceAdapter`/`selectAuthAdapter` against specific configurations\n * use this instead — keeping results independent of any developer `.env.local`.\n */\nexport function setStudioEnvOverrideForTests(env: EnvRecord | null): void {\n envOverride = env\n}\n\n// ---------------------------------------------------------------------------\n// Consumer-provided env (EC-357 — the Vite dep-optimizer env fix)\n// ---------------------------------------------------------------------------\n\n/** Normalized mount-provided env: the string entries + the captured `DEV` flag. */\ninterface ProvidedStudioEnv {\n record: EnvRecord\n dev: boolean | undefined\n}\n\nlet providedEnv: ProvidedStudioEnv | null = null\n\n/**\n * Install the CONSUMER-captured env as the studio's env source (EC-357). The root\n * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside\n * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on\n * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the\n * backend URL / mode / dev-login silently vanish and every sign-in fails), and on\n * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).\n *\n * The consumer's own entry module is code Vite ALWAYS transforms live, so the env\n * is captured there and handed down: `createStudio(config, studioConfig, { env:\n * import.meta.env })`. When a provided env exists it REPLACES the engine-visible\n * Vite layer entirely (never merged with it — a stale optimizer snapshot must not\n * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.\n *\n * `DEV` is captured alongside (it gates dev auto-login): in a production consumer\n * build Vite statically inlines `import.meta.env` with `DEV: false`, so the\n * dev-login gate keeps failing closed exactly as before. Non-string values other\n * than `DEV` are dropped — the studio env surface is a string record.\n *\n * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option\n * keeps today's engine-visible fallback behavior, plus a loud runtime warning when\n * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).\n */\nexport function provideStudioEnv(env: Record<string, unknown> | null | undefined): void {\n if (env === null || env === undefined) return\n const record: EnvRecord = {}\n for (const [key, value] of Object.entries(env)) {\n if (typeof value === 'string') record[key] = value\n }\n providedEnv = {\n record,\n dev: env.DEV === true ? true : env.DEV === false ? false : undefined,\n }\n maybeWarnEngineEnvStale()\n}\n\n/** Test seam: clear the mount-provided env. Never used by app code. */\nexport function resetProvidedStudioEnvForTests(): void {\n providedEnv = null\n warnedStale = false\n warnedUnreachable = false\n}\n\n/**\n * Resolve the configured studio env. Precedence: the test override, else the\n * mount-provided env (over `process.env`), else the engine-visible Vite env\n * (over `process.env`).\n */\nexport function studioEnv(): EnvRecord {\n if (envOverride) return envOverride\n if (providedEnv) return { ...nodeEnv(), ...providedEnv.record }\n maybeWarnEngineEnvUnreachable()\n return { ...nodeEnv(), ...viteEnv() }\n}\n\n// --- Boot-time env self-check (EC-357, Oli: \"easier to catch\") ---------------\n\n/** The env keys whose loss/staleness breaks boot or sign-in. */\nexport const STUDIO_ENV_KEYS: readonly string[] = [\n 'VITE_ELYTRA_MODE',\n 'VITE_CONVEX_URL',\n 'VITE_ELYTRA_DEV_LOGIN',\n]\n\nexport type StudioEnvAnomaly =\n /** No mount-provided env AND the engine module has no `import.meta.env` at all. */\n | { kind: 'engine-env-unreachable' }\n /** The engine-visible env disagrees with the mount-provided env (stale optimizer cache). */\n | { kind: 'engine-env-stale'; keys: string[] }\n\n/**\n * Pure anomaly detection over the two env views (unit-testable core of the boot\n * warnings). `engineVisible === null` means the engine module has no\n * `import.meta.env` — expected under dep optimization WITH a provided env (silent),\n * fatal without one (the values can never arrive). A stale mismatch fires only for\n * keys the ENGINE sees with a different value than the mount provided (the Vite 7\n * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided\n * env is authoritative and in effect.\n */\nexport function detectStudioEnvAnomaly(\n provided: EnvRecord | null,\n engineVisible: EnvRecord | null,\n inBrowser: boolean,\n): StudioEnvAnomaly | null {\n if (!inBrowser) return null\n if (!provided) return engineVisible === null ? { kind: 'engine-env-unreachable' } : null\n if (engineVisible === null) return null\n const stale = STUDIO_ENV_KEYS.filter((key) => {\n const engineValue = readEnv(engineVisible, key)\n return engineValue !== undefined && engineValue !== readEnv(provided, key)\n })\n return stale.length > 0 ? { kind: 'engine-env-stale', keys: stale } : null\n}\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\nlet warnedUnreachable = false\nfunction maybeWarnEngineEnvUnreachable(): void {\n if (warnedUnreachable) return\n const anomaly = detectStudioEnvAnomaly(null, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-unreachable') return\n warnedUnreachable = true\n console.warn(\n '[elytra] The engine cannot see import.meta.env — this @elytracms module was dep-optimized by Vite, ' +\n 'so VITE_CONVEX_URL / VITE_ELYTRA_MODE / VITE_ELYTRA_DEV_LOGIN cannot reach it at runtime. ' +\n 'Fix: pass your studio entry’s env to the mount — createStudio(config, studioConfig, { env: import.meta.env }) — ' +\n \"or add optimizeDeps.exclude: ['@elytracms/studio', '@elytracms/core'] to the studio vite.config.ts.\",\n )\n}\n\nlet warnedStale = false\nfunction maybeWarnEngineEnvStale(): void {\n if (warnedStale || !providedEnv) return\n const anomaly = detectStudioEnvAnomaly(providedEnv.record, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-stale') return\n warnedStale = true\n console.warn(\n `[elytra] import.meta.env inside the engine disagrees with the studio mount's env for: ${anomaly.keys.join(', ')}. ` +\n \"The engine copy is a stale Vite dep-optimizer snapshot; the mount-provided values are in effect. \" +\n 'Delete node_modules/.vite to drop the stale copy.',\n )\n}\n\n/**\n * Pure mode decision — testable with a plain env record. The app calls it with\n * {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools.\n */\nexport function resolveBackendConfig(env: EnvRecord = studioEnv()): StudioBackendConfig {\n const mode = readEnv(env, MODE_ENV_VAR)\n if (mode === PLAYGROUND_MODE) return { kind: 'playground' }\n if (mode !== undefined) {\n return {\n kind: 'misconfigured',\n reason: 'unknown-mode',\n detail: `${MODE_ENV_VAR}=\"${mode}\" is not a known mode. The only supported value is \"${PLAYGROUND_MODE}\"; unset it to run against Convex.`,\n }\n }\n const url = readEnv(env, CONVEX_URL_ENV_VAR) ?? readEnv(env, 'CONVEX_URL')\n if (url) return { kind: 'convex', url }\n return {\n kind: 'misconfigured',\n reason: 'missing-convex-url',\n // EC-357: name BOTH causes — the var may be genuinely unset, or set but never\n // reaching the client at runtime (Vite dep-optimized engine code; see\n // `provideStudioEnv`). The paired fix text (deployment-ready `FIX.backendUrl`)\n // carries the full remedy for the second cause.\n detail: `No persistence backend is configured — ${CONVEX_URL_ENV_VAR} is unset or is not reaching the client at runtime. Set it to your Convex deployment URL (see docs/backend-setup.md), or opt into the ephemeral local playground with ${MODE_ENV_VAR}=${PLAYGROUND_MODE}.`,\n }\n}\n\n/** True when the studio runs in the explicit, labeled playground mode. */\nexport function isPlaygroundMode(): boolean {\n return resolveBackendConfig().kind === 'playground'\n}\n\n// ---------------------------------------------------------------------------\n// Dev auto-login (unattended testing — NOT an auth bypass)\n// ---------------------------------------------------------------------------\n\n/** `email:password` for unattended dev sign-in (gitignored `.env.local`). */\nexport const DEV_LOGIN_ENV_VAR = 'VITE_ELYTRA_DEV_LOGIN'\n\n/** Parsed dev auto-login credentials. */\nexport interface DevAutoLoginCredentials {\n email: string\n password: string\n}\n\n/**\n * Is this a Vite *dev* build? The mount-provided env's captured `DEV` flag wins\n * (EC-357 — inside dep-optimized engine code the engine-visible flag is absent on\n * Vite ≥8 and frozen on Vite 7); the consumer's `import.meta.env.DEV` is statically\n * `true` under `vite dev` and statically `false` in any production build, so the\n * dev auto-login gate still fails closed in production. Without a provided env the\n * engine-visible static flag applies as before (where it also dead-code-eliminates\n * the feature out of a directly-bundled production build).\n */\nfunction isViteDevBuild(): boolean {\n if (providedEnv?.dev !== undefined) return providedEnv.dev\n if (typeof import.meta === 'undefined') return false\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n}\n\n/**\n * The studio's house dev-build check, public: dev-only surfaces (e.g. the\n * EC-414 canvas-theme migration notice) gate on THIS — it honors the\n * mount-provided env's captured `DEV` flag (the EC-357 dep-optimization\n * nuance) before falling back to the engine-visible static flag.\n */\nexport function isDevRuntime(): boolean {\n return isViteDevBuild()\n}\n\n/**\n * Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=\n * email:password` is set in a dev build, the studio auto-signs-in with those\n * credentials on load (see {@link DevAutoLoginAdapter}) so local/automated\n * testing skips the sign-in screen while still establishing a REAL backend\n * session: Convex's server-side `requireRole` guards still apply, so the dev\n * user must exist and be a member. Returns null in production builds (gated by\n * {@link isViteDevBuild}) and when unset/malformed, so it can never weaken a\n * deployed app. `isDevBuild` is a parameter purely so the decision is unit\n * testable (`import.meta.env.DEV` is not reachable via `vi.stubEnv`).\n *\n * `password` may itself contain `:` — only the first colon separates the fields.\n */\nexport function resolveDevAutoLogin(\n env: EnvRecord = studioEnv(),\n isDevBuild: boolean = isViteDevBuild(),\n): DevAutoLoginCredentials | null {\n if (!isDevBuild) return null\n const raw = readEnv(env, DEV_LOGIN_ENV_VAR)\n if (!raw) return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const email = raw.slice(0, separator).trim()\n const password = raw.slice(separator + 1)\n if (!email || !password) return null\n return { email, password }\n}\n\n/** True when dev auto-login is configured (dev build only) — for the UI marker. */\nexport function isDevAutoLoginActive(): boolean {\n return resolveDevAutoLogin() !== null\n}\n"]}
|
|
1
|
+
{"version":3,"file":"backend-mode.js","sourceRoot":"","sources":["../../../src/lib/persistence/backend-mode.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Studio backend mode resolution (EC-153). Convex is the **default and primary**\n * persistence backend; the deterministic in-memory adapter is demoted to an\n * explicit, labeled **playground** mode and is never an automatic fallback:\n *\n * - `VITE_CONVEX_URL` set → Convex mode (the production/dev default).\n * - `VITE_ELYTRA_MODE=playground` → playground mode (seeded in-memory adapter,\n * local + ephemeral). The flag wins over a configured URL — explicit beats\n * implicit — so a checked-out `.env.local` can be flipped without deleting it.\n * - Neither → **misconfigured**: the studio renders a deterministic boot-error\n * screen (see `DeploymentSetupGate`) telling the developer exactly what to set.\n * Likewise, a Convex backend that is unreachable at runtime is a visible\n * error state — the studio never silently swaps in in-memory storage.\n *\n * EC-603: the RULE above lives in ONE place — core's `parseBackendMode`, which the\n * `elytra` CLI runs over the same env bytes. This module keeps the studio's env SOURCE\n * (the mount-provided env, {@link studioEnv}) and hands it to that parser.\n */\nimport {\n BACKEND_MODE_ENV_VAR,\n BACKEND_URL_ENV_VAR,\n PLAYGROUND_MODE as CORE_PLAYGROUND_MODE,\n parseBackendMode,\n readEnvValue,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport type { BackendMode } from '@elytracms/core/export-sync/deployment-ready'\n\nexport const MODE_ENV_VAR = BACKEND_MODE_ENV_VAR\nexport const PLAYGROUND_MODE = CORE_PLAYGROUND_MODE\nexport const CONVEX_URL_ENV_VAR = BACKEND_URL_ENV_VAR\n\nexport type StudioBackendMode = 'convex' | 'playground'\n\n/** The backend verdict — core's {@link BackendMode}, under the studio's historical name. */\nexport type StudioBackendConfig = BackendMode\n\n/** The studio's env surface: Vite client env first, Node/SSR env second. */\nexport type EnvRecord = Record<string, string | undefined>\n\n/**\n * The ENGINE-visible `import.meta.env`, or `null` when the module has none at all.\n * `null` is a meaningful signal (EC-357): every module Vite transforms live gets an\n * `import.meta.env`, so its complete absence in a browser means this engine module\n * was served from Vite's dep-optimizer cache — where Vite ≥8 no longer inlines\n * custom `VITE_*` values (and Vite 7 froze them at optimize time). Engine code must\n * therefore never trust this view when a mount-provided env exists — see\n * {@link provideStudioEnv}.\n */\nfunction engineVisibleEnv(): EnvRecord | null {\n if (typeof import.meta === 'undefined') return null\n return (import.meta as { env?: EnvRecord }).env ?? null\n}\n\nfunction viteEnv(): EnvRecord {\n return engineVisibleEnv() ?? {}\n}\n\nfunction nodeEnv(): EnvRecord {\n return typeof process !== 'undefined' ? (process.env ?? {}) : {}\n}\n\n/** Trimmed, blank-is-absent — the parser's own read, so every studio env key reads alike. */\nconst readEnv: (env: EnvRecord, key: string) => string | undefined = readEnvValue\n\nlet envOverride: EnvRecord | null = null\n\n/**\n * Test seam: pin the env the selectors see. `vi.stubEnv` cannot reach the\n * `import.meta.env` snapshot Vitest hands this module, so suites that exercise\n * `selectPersistenceAdapter`/`selectAuthAdapter` against specific configurations\n * use this instead — keeping results independent of any developer `.env.local`.\n */\nexport function setStudioEnvOverrideForTests(env: EnvRecord | null): void {\n envOverride = env\n}\n\n// ---------------------------------------------------------------------------\n// Consumer-provided env (EC-357 — the Vite dep-optimizer env fix)\n// ---------------------------------------------------------------------------\n\n/** Normalized mount-provided env: the string entries + the captured `DEV` flag. */\ninterface ProvidedStudioEnv {\n record: EnvRecord\n dev: boolean | undefined\n}\n\nlet providedEnv: ProvidedStudioEnv | null = null\n\n/**\n * Install the CONSUMER-captured env as the studio's env source (EC-357). The root\n * cause this fixes: the engine used to read `import.meta.env.VITE_*` inside\n * `@elytracms` package code, which Vite dep-optimizes in a real consumer — on\n * Vite ≥8 those values are simply absent at runtime in the optimized chunk (the\n * backend URL / mode / dev-login silently vanish and every sign-in fails), and on\n * Vite 7 they froze at optimize time (stale until `node_modules/.vite` is wiped).\n *\n * The consumer's own entry module is code Vite ALWAYS transforms live, so the env\n * is captured there and handed down: `createStudio(config, studioConfig, { env:\n * import.meta.env })`. When a provided env exists it REPLACES the engine-visible\n * Vite layer entirely (never merged with it — a stale optimizer snapshot must not\n * shadow a removed key); `process.env` stays underneath as the SSR/tooling base.\n *\n * `DEV` is captured alongside (it gates dev auto-login): in a production consumer\n * build Vite statically inlines `import.meta.env` with `DEV: false`, so the\n * dev-login gate keeps failing closed exactly as before. Non-string values other\n * than `DEV` are dropped — the studio env surface is a string record.\n *\n * Passing `null`/`undefined` is a no-op (a consumer entry that predates the option\n * keeps today's engine-visible fallback behavior, plus a loud runtime warning when\n * that fallback is provably broken — see {@link maybeWarnEngineEnvUnreachable}).\n */\nexport function provideStudioEnv(env: Record<string, unknown> | null | undefined): void {\n if (env === null || env === undefined) return\n const record: EnvRecord = {}\n for (const [key, value] of Object.entries(env)) {\n if (typeof value === 'string') record[key] = value\n }\n providedEnv = {\n record,\n dev: env.DEV === true ? true : env.DEV === false ? false : undefined,\n }\n maybeWarnEngineEnvStale()\n}\n\n/** Test seam: clear the mount-provided env. Never used by app code. */\nexport function resetProvidedStudioEnvForTests(): void {\n providedEnv = null\n warnedStale = false\n warnedUnreachable = false\n}\n\n/**\n * Resolve the configured studio env. Precedence: the test override, else the\n * mount-provided env (over `process.env`), else the engine-visible Vite env\n * (over `process.env`).\n */\nexport function studioEnv(): EnvRecord {\n if (envOverride) return envOverride\n if (providedEnv) return { ...nodeEnv(), ...providedEnv.record }\n maybeWarnEngineEnvUnreachable()\n return { ...nodeEnv(), ...viteEnv() }\n}\n\n// --- Boot-time env self-check (EC-357, Oli: \"easier to catch\") ---------------\n\n/** The env keys whose loss/staleness breaks boot or sign-in. */\nexport const STUDIO_ENV_KEYS: readonly string[] = [\n 'VITE_ELYTRA_MODE',\n 'VITE_CONVEX_URL',\n 'VITE_ELYTRA_DEV_LOGIN',\n]\n\nexport type StudioEnvAnomaly =\n /** No mount-provided env AND the engine module has no `import.meta.env` at all. */\n | { kind: 'engine-env-unreachable' }\n /** The engine-visible env disagrees with the mount-provided env (stale optimizer cache). */\n | { kind: 'engine-env-stale'; keys: string[] }\n\n/**\n * Pure anomaly detection over the two env views (unit-testable core of the boot\n * warnings). `engineVisible === null` means the engine module has no\n * `import.meta.env` — expected under dep optimization WITH a provided env (silent),\n * fatal without one (the values can never arrive). A stale mismatch fires only for\n * keys the ENGINE sees with a different value than the mount provided (the Vite 7\n * freeze); keys the engine simply lacks (Vite 8) are not anomalies — the provided\n * env is authoritative and in effect.\n */\nexport function detectStudioEnvAnomaly(\n provided: EnvRecord | null,\n engineVisible: EnvRecord | null,\n inBrowser: boolean,\n): StudioEnvAnomaly | null {\n if (!inBrowser) return null\n if (!provided) return engineVisible === null ? { kind: 'engine-env-unreachable' } : null\n if (engineVisible === null) return null\n const stale = STUDIO_ENV_KEYS.filter((key) => {\n const engineValue = readEnv(engineVisible, key)\n return engineValue !== undefined && engineValue !== readEnv(provided, key)\n })\n return stale.length > 0 ? { kind: 'engine-env-stale', keys: stale } : null\n}\n\nfunction inBrowser(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined'\n}\n\nlet warnedUnreachable = false\nfunction maybeWarnEngineEnvUnreachable(): void {\n if (warnedUnreachable) return\n const anomaly = detectStudioEnvAnomaly(null, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-unreachable') return\n warnedUnreachable = true\n console.warn(\n '[elytra] The engine cannot see import.meta.env — this @elytracms module was dep-optimized by Vite, ' +\n 'so VITE_CONVEX_URL / VITE_ELYTRA_MODE / VITE_ELYTRA_DEV_LOGIN cannot reach it at runtime. ' +\n 'Fix: pass your studio entry’s env to the mount — createStudio(config, studioConfig, { env: import.meta.env }) — ' +\n \"or add optimizeDeps.exclude: ['@elytracms/studio', '@elytracms/core'] to the studio vite.config.ts.\",\n )\n}\n\nlet warnedStale = false\nfunction maybeWarnEngineEnvStale(): void {\n if (warnedStale || !providedEnv) return\n const anomaly = detectStudioEnvAnomaly(providedEnv.record, engineVisibleEnv(), inBrowser())\n if (anomaly?.kind !== 'engine-env-stale') return\n warnedStale = true\n console.warn(\n `[elytra] import.meta.env inside the engine disagrees with the studio mount's env for: ${anomaly.keys.join(', ')}. ` +\n \"The engine copy is a stale Vite dep-optimizer snapshot; the mount-provided values are in effect. \" +\n 'Delete node_modules/.vite to drop the stale copy.',\n )\n}\n\n/**\n * Pure mode decision — testable with a plain env record. The app calls it with\n * {@link studioEnv}; `CONVEX_URL` (Node-side alias) is honored for SSR/tools. The rule\n * itself is core's `parseBackendMode` (EC-603) — the same one the CLI applies.\n */\nexport function resolveBackendConfig(env: EnvRecord = studioEnv()): StudioBackendConfig {\n return parseBackendMode(env)\n}\n\n/** True when the studio runs in the explicit, labeled playground mode. */\nexport function isPlaygroundMode(): boolean {\n return resolveBackendConfig().kind === 'playground'\n}\n\n// ---------------------------------------------------------------------------\n// Dev auto-login (unattended testing — NOT an auth bypass)\n// ---------------------------------------------------------------------------\n\n/** `email:password` for unattended dev sign-in (gitignored `.env.local`). */\nexport const DEV_LOGIN_ENV_VAR = 'VITE_ELYTRA_DEV_LOGIN'\n\n/** Parsed dev auto-login credentials. */\nexport interface DevAutoLoginCredentials {\n email: string\n password: string\n}\n\n/**\n * Is this a Vite *dev* build? The mount-provided env's captured `DEV` flag wins\n * (EC-357 — inside dep-optimized engine code the engine-visible flag is absent on\n * Vite ≥8 and frozen on Vite 7); the consumer's `import.meta.env.DEV` is statically\n * `true` under `vite dev` and statically `false` in any production build, so the\n * dev auto-login gate still fails closed in production. Without a provided env the\n * engine-visible static flag applies as before (where it also dead-code-eliminates\n * the feature out of a directly-bundled production build).\n */\nfunction isViteDevBuild(): boolean {\n if (providedEnv?.dev !== undefined) return providedEnv.dev\n if (typeof import.meta === 'undefined') return false\n return (import.meta as { env?: { DEV?: boolean } }).env?.DEV === true\n}\n\n/**\n * The studio's house dev-build check, public: dev-only surfaces (e.g. the\n * EC-414 canvas-theme migration notice) gate on THIS — it honors the\n * mount-provided env's captured `DEV` flag (the EC-357 dep-optimization\n * nuance) before falling back to the engine-visible static flag.\n */\nexport function isDevRuntime(): boolean {\n return isViteDevBuild()\n}\n\n/**\n * Dev-only unattended auth — **not a bypass**. When `VITE_ELYTRA_DEV_LOGIN=\n * email:password` is set in a dev build, the studio auto-signs-in with those\n * credentials on load (see {@link DevAutoLoginAdapter}) so local/automated\n * testing skips the sign-in screen while still establishing a REAL backend\n * session: Convex's server-side `requireRole` guards still apply, so the dev\n * user must exist and be a member. Returns null in production builds (gated by\n * {@link isViteDevBuild}) and when unset/malformed, so it can never weaken a\n * deployed app. `isDevBuild` is a parameter purely so the decision is unit\n * testable (`import.meta.env.DEV` is not reachable via `vi.stubEnv`).\n *\n * `password` may itself contain `:` — only the first colon separates the fields.\n */\nexport function resolveDevAutoLogin(\n env: EnvRecord = studioEnv(),\n isDevBuild: boolean = isViteDevBuild(),\n): DevAutoLoginCredentials | null {\n if (!isDevBuild) return null\n const raw = readEnv(env, DEV_LOGIN_ENV_VAR)\n if (!raw) return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const email = raw.slice(0, separator).trim()\n const password = raw.slice(separator + 1)\n if (!email || !password) return null\n return { email, password }\n}\n\n/** True when dev auto-login is configured (dev build only) — for the UI marker. */\nexport function isDevAutoLoginActive(): boolean {\n return resolveDevAutoLogin() !== null\n}\n"]}
|