meno-core 1.0.54 → 1.1.1
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/bin/cli.ts +19 -205
- package/build-astro.ts +31 -27
- package/dist/bin/cli.js +15 -154
- package/dist/bin/cli.js.map +2 -2
- package/dist/chunks/{chunk-3XER4E5W.js → chunk-4ZRU52J2.js} +6 -5
- package/dist/chunks/{chunk-3XER4E5W.js.map → chunk-4ZRU52J2.js.map} +2 -2
- package/dist/chunks/{chunk-NVRBTSQG.js → chunk-5X4HCN7J.js} +4 -4
- package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
- package/dist/chunks/{chunk-FZITJSSS.js → chunk-7CCVOL5M.js} +14 -14
- package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
- package/dist/chunks/{chunk-AE3QK5QW.js → chunk-7ZLF4NE5.js} +31 -26
- package/dist/chunks/chunk-7ZLF4NE5.js.map +7 -0
- package/dist/chunks/{chunk-HIZMY3EP.js → chunk-AR7LHIFY.js} +8 -2
- package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
- package/dist/chunks/{chunk-7HWQUVTU.js → chunk-J4IPTP5X.js} +1839 -443
- package/dist/chunks/chunk-J4IPTP5X.js.map +7 -0
- package/dist/chunks/{chunk-5ETZFREW.js → chunk-QWTQZHG3.js} +186 -79
- package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
- package/dist/chunks/{chunk-STDY3OVM.js → chunk-XTKNX4FW.js} +3 -3
- package/dist/chunks/{chunk-STDY3OVM.js.map → chunk-XTKNX4FW.js.map} +2 -2
- package/dist/chunks/{fs-ZI5JEU7V.js → fs-G3ANWEFP.js} +2 -2
- package/dist/lib/client/index.js +381 -159
- package/dist/lib/client/index.js.map +3 -3
- package/dist/lib/server/index.js +14365 -6276
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +296 -87
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/shared/richtext/index.js +1 -1
- package/dist/lib/test-utils/index.js +27 -21
- package/dist/lib/test-utils/index.js.map +2 -2
- package/lib/client/ErrorBoundary.test.tsx +147 -90
- package/lib/client/ErrorBoundary.tsx +8 -5
- package/lib/client/componentRegistry.ts +0 -1
- package/lib/client/core/ComponentBuilder.test.ts +150 -0
- package/lib/client/core/ComponentBuilder.ts +112 -12
- package/lib/client/core/ComponentRenderer.test.tsx +1 -2
- package/lib/client/core/builders/embedBuilder.ts +1 -1
- package/lib/client/core/builders/linkBuilder.ts +2 -2
- package/lib/client/core/builders/linkNodeBuilder.ts +1 -1
- package/lib/client/core/builders/listBuilder.ts +41 -4
- package/lib/client/core/builders/localeListBuilder.ts +2 -2
- package/lib/client/core/cmsTemplateProcessor.ts +1 -2
- package/lib/client/hmr/HMRManager.tsx +39 -30
- package/lib/client/hmrCssReload.ts +35 -6
- package/lib/client/hmrWebSocket.ts +5 -5
- package/lib/client/hooks/useColorVariables.ts +12 -9
- package/lib/client/hooks/usePropertyAutocomplete.ts +1 -1
- package/lib/client/hooks/useVariables.ts +3 -1
- package/lib/client/meno-filter/MenoFilter.test.ts +28 -28
- package/lib/client/meno-filter/MenoFilter.ts +9 -6
- package/lib/client/meno-filter/bindings.ts +3 -3
- package/lib/client/meno-filter/init.ts +3 -3
- package/lib/client/meno-filter/renderer.ts +8 -4
- package/lib/client/meno-filter/ui.ts +1 -1
- package/lib/client/meno-filter/updates.ts +3 -2
- package/lib/client/meno-filter/utils.ts +1 -1
- package/lib/client/navigation.test.ts +40 -44
- package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
- package/lib/client/responsiveStyleResolver.test.ts +5 -5
- package/lib/client/responsiveStyleResolver.ts +61 -4
- package/lib/client/routing/RouteLoader.test.ts +1 -1
- package/lib/client/routing/RouteLoader.ts +8 -3
- package/lib/client/routing/Router.tsx +181 -152
- package/lib/client/scripts/ScriptExecutor.ts +3 -3
- package/lib/client/services/PrefetchService.test.ts +1 -1
- package/lib/client/services/PrefetchService.ts +13 -5
- package/lib/client/styleProcessor.ts +9 -3
- package/lib/client/styles/StyleInjector.ts +1 -1
- package/lib/client/styles/UtilityClassCollector.ts +54 -13
- package/lib/client/templateEngine.test.ts +17 -23
- package/lib/client/templateEngine.ts +7 -13
- package/lib/client/theme.test.ts +1 -1
- package/lib/server/__integration__/server-lifecycle.test.ts +3 -1
- package/lib/server/__integration__/static-assets.test.ts +2 -2
- package/lib/server/__integration__/test-helpers.ts +3 -4
- package/lib/server/astro/cmsPageEmitter.ts +4 -5
- package/lib/server/astro/componentEmitter.ts +11 -8
- package/lib/server/astro/nodeToAstro.test.ts +4 -4
- package/lib/server/astro/nodeToAstro.ts +41 -36
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +11 -11
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +7 -6
- package/lib/server/astro/pageEmitter.ts +1 -1
- package/lib/server/astro/templateTransformer.ts +3 -3
- package/lib/server/createServer.ts +2 -2
- package/lib/server/cssGenerator.ts +2 -2
- package/lib/server/fileWatcher.test.ts +41 -4
- package/lib/server/fileWatcher.ts +102 -23
- package/lib/server/index.ts +7 -16
- package/lib/server/jsonLoader.test.ts +3 -3
- package/lib/server/jsonLoader.ts +8 -5
- package/lib/server/middleware/cors.test.ts +1 -1
- package/lib/server/middleware/cors.ts +2 -2
- package/lib/server/middleware/errorHandler.test.ts +2 -2
- package/lib/server/middleware/logger.test.ts +3 -3
- package/lib/server/middleware/logger.ts +7 -7
- package/lib/server/migrateTemplates.ts +3 -3
- package/lib/server/projectContext.ts +1 -1
- package/lib/server/providers/fileSystemCMSProvider.test.ts +18 -4
- package/lib/server/providers/fileSystemCMSProvider.ts +27 -18
- package/lib/server/routes/api/cms.ts +1 -1
- package/lib/server/routes/api/colors.test.ts +2 -2
- package/lib/server/routes/api/components.ts +1 -1
- package/lib/server/routes/api/config.ts +3 -3
- package/lib/server/routes/api/core-routes.ts +2 -2
- package/lib/server/routes/api/enums.test.ts +2 -2
- package/lib/server/routes/api/functions.ts +1 -1
- package/lib/server/routes/api/variables.test.ts +2 -2
- package/lib/server/routes/index.ts +2 -2
- package/lib/server/routes/pages.ts +1 -2
- package/lib/server/routes/static.ts +1 -2
- package/lib/server/runtime/bundler.ts +65 -43
- package/lib/server/runtime/fs.ts +13 -13
- package/lib/server/runtime/httpServer.ts +3 -3
- package/lib/server/services/ColorService.test.ts +3 -3
- package/lib/server/services/ColorService.ts +5 -2
- package/lib/server/services/EnumService.test.ts +2 -2
- package/lib/server/services/EnumService.ts +4 -1
- package/lib/server/services/VariableService.test.ts +7 -7
- package/lib/server/services/VariableService.ts +5 -2
- package/lib/server/services/cmsService.test.ts +44 -44
- package/lib/server/services/cmsService.ts +17 -97
- package/lib/server/services/componentService.test.ts +10 -16
- package/lib/server/services/componentService.ts +20 -12
- package/lib/server/services/configService.test.ts +13 -14
- package/lib/server/services/configService.ts +30 -0
- package/lib/server/services/fileWatcherService.ts +2 -2
- package/lib/server/services/pageService.test.ts +7 -7
- package/lib/server/services/pageService.ts +18 -5
- package/lib/server/ssr/attributeBuilder.ts +2 -2
- package/lib/server/ssr/cmsSSRProcessor.ts +1 -2
- package/lib/server/ssr/errorOverlay.ts +1 -1
- package/lib/server/ssr/htmlGenerator.nonce.test.ts +6 -5
- package/lib/server/ssr/htmlGenerator.test.ts +0 -1
- package/lib/server/ssr/htmlGenerator.ts +25 -9
- package/lib/server/ssr/imageMetadata.ts +2 -2
- package/lib/server/ssr/jsCollector.test.ts +5 -5
- package/lib/server/ssr/liveReloadIntegration.test.ts +7 -8
- package/lib/server/ssr/ssrRenderer.branches.test.ts +9 -9
- package/lib/server/ssr/ssrRenderer.test.ts +150 -15
- package/lib/server/ssr/ssrRenderer.ts +218 -40
- package/lib/server/ssrRenderer.test.ts +6 -6
- package/lib/server/utils/jsonLineMapper.ts +7 -7
- package/lib/server/validateStyleCoverage.ts +0 -1
- package/lib/server/websocketManager.ts +12 -0
- package/lib/shared/breakpoints.test.ts +5 -5
- package/lib/shared/cmsQuery.test.ts +81 -0
- package/lib/shared/cmsQuery.ts +92 -0
- package/lib/shared/cmsQueryParser.ts +11 -7
- package/lib/shared/colorConversions.ts +9 -9
- package/lib/shared/colorVariableUtils.test.ts +22 -0
- package/lib/shared/colorVariableUtils.ts +19 -5
- package/lib/shared/constants.ts +4 -0
- package/lib/shared/cssGeneration.test.ts +33 -0
- package/lib/shared/cssGeneration.ts +50 -35
- package/lib/shared/cssProperties.test.ts +64 -64
- package/lib/shared/cssProperties.ts +2 -2
- package/lib/shared/elementClassName.ts +2 -2
- package/lib/shared/errorLogger.test.ts +12 -12
- package/lib/shared/expressionEvaluator.test.ts +27 -1
- package/lib/shared/expressionEvaluator.ts +9 -0
- package/lib/shared/fontLoader.test.ts +13 -13
- package/lib/shared/fontLoader.ts +2 -2
- package/lib/shared/gradientUtils.test.ts +2 -2
- package/lib/shared/gradientUtils.ts +15 -8
- package/lib/shared/hrefRefs.test.ts +2 -2
- package/lib/shared/hrefRefs.ts +1 -1
- package/lib/shared/i18n.test.ts +3 -3
- package/lib/shared/i18n.ts +5 -4
- package/lib/shared/index.ts +29 -0
- package/lib/shared/inlineSvgStyleRules.ts +12 -9
- package/lib/shared/interactiveStyleMappings.test.ts +21 -21
- package/lib/shared/interactiveStyleMappings.ts +2 -8
- package/lib/shared/interactiveStyles.test.ts +3 -1
- package/lib/shared/itemTemplateUtils.test.ts +5 -5
- package/lib/shared/itemTemplateUtils.ts +9 -5
- package/lib/shared/jsonRepair.ts +8 -8
- package/lib/shared/libraryLoader.test.ts +6 -6
- package/lib/shared/linkUtils.ts +1 -1
- package/lib/shared/logger.test.ts +66 -0
- package/lib/shared/logger.ts +94 -0
- package/lib/shared/markdown.ts +25 -0
- package/lib/shared/nodeUtils.test.ts +13 -9
- package/lib/shared/nodeUtils.ts +27 -4
- package/lib/shared/pathSecurity.ts +1 -1
- package/lib/shared/pathUtils.ts +1 -2
- package/lib/shared/paths/PathConverter.ts +3 -3
- package/lib/shared/paths/PathUtils.ts +4 -3
- package/lib/shared/paths/PathValidator.ts +1 -1
- package/lib/shared/permissions.test.ts +168 -0
- package/lib/shared/permissions.ts +162 -0
- package/lib/shared/propResolver.test.ts +1 -1
- package/lib/shared/propResolver.ts +1 -1
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +5 -3
- package/lib/shared/registry/ClientRegistry.ts +0 -1
- package/lib/shared/registry/ComponentRegistry.test.ts +3 -3
- package/lib/shared/registry/ComponentRegistry.ts +6 -1
- package/lib/shared/registry/NodeTypeDefinition.ts +2 -2
- package/lib/shared/registry/SSRRegistry.ts +0 -1
- package/lib/shared/registry/fieldPresets.ts +3 -1
- package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +1 -1
- package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +43 -7
- package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -1
- package/lib/shared/registry/nodeTypes/index.ts +16 -1
- package/lib/shared/responsiveScaling.test.ts +1 -3
- package/lib/shared/responsiveScaling.ts +6 -5
- package/lib/shared/responsiveStyleUtils.test.ts +0 -1
- package/lib/shared/richtext/htmlToTiptap.test.ts +116 -116
- package/lib/shared/richtext/htmlToTiptap.ts +1 -1
- package/lib/shared/styleNodeUtils.ts +17 -14
- package/lib/shared/tailwindThemeScale.ts +256 -0
- package/lib/shared/themeDefaults.test.ts +0 -3
- package/lib/shared/tree/PathBuilder.test.ts +1 -7
- package/lib/shared/tree/PathBuilder.ts +15 -13
- package/lib/shared/treePathUtils.test.ts +49 -2
- package/lib/shared/treePathUtils.ts +40 -4
- package/lib/shared/types/api.ts +105 -7
- package/lib/shared/types/cms.test.ts +34 -0
- package/lib/shared/types/cms.ts +65 -13
- package/lib/shared/types/components.ts +24 -0
- package/lib/shared/types/errors.test.ts +1 -1
- package/lib/shared/types/index.ts +23 -0
- package/lib/shared/types/permissions.ts +139 -0
- package/lib/shared/types/variables.ts +3 -3
- package/lib/shared/utilityClassConfig.ts +219 -0
- package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
- package/lib/shared/utilityClassMapper.test.ts +401 -26
- package/lib/shared/utilityClassMapper.ts +193 -29
- package/lib/shared/utilityClassNames.test.ts +38 -0
- package/lib/shared/utilityClassNames.ts +301 -23
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/validation/permissionsValidators.test.ts +49 -0
- package/lib/shared/validation/permissionsValidators.ts +49 -0
- package/lib/shared/validation/propValidator.test.ts +1 -1
- package/lib/shared/validation/propValidator.ts +1 -1
- package/lib/shared/validation/schemas.test.ts +41 -0
- package/lib/shared/validation/schemas.ts +159 -13
- package/lib/shared/validation/validators.test.ts +2 -2
- package/lib/shared/viewportUnits.test.ts +13 -0
- package/lib/shared/viewportUnits.ts +12 -5
- package/lib/test-utils/factories/ConsoleMockFactory.ts +7 -7
- package/lib/test-utils/factories/DomMockFactory.ts +10 -4
- package/lib/test-utils/factories/ServerMockFactory.ts +1 -1
- package/lib/test-utils/factories/StoreMockFactory.ts +5 -5
- package/lib/test-utils/mockFactories.ts +1 -1
- package/lib/test-utils/mocks.ts +3 -3
- package/package.json +3 -5
- package/scripts/build-for-publish.mjs +2 -4
- package/scripts/build-meno-filter.ts +3 -3
- package/tsconfig.json +1 -0
- package/vite.config.ts +1 -1
- package/build-next.ts +0 -1374
- package/build-static.test.ts +0 -453
- package/build-static.ts +0 -1072
- package/dist/build-static.js +0 -37
- package/dist/chunks/chunk-2FN4UOVO.js +0 -6465
- package/dist/chunks/chunk-2FN4UOVO.js.map +0 -7
- package/dist/chunks/chunk-5ETZFREW.js.map +0 -7
- package/dist/chunks/chunk-7E4IF5L7.js +0 -245
- package/dist/chunks/chunk-7E4IF5L7.js.map +0 -7
- package/dist/chunks/chunk-7HWQUVTU.js.map +0 -7
- package/dist/chunks/chunk-AE3QK5QW.js.map +0 -7
- package/dist/chunks/chunk-F6KTJYGV.js +0 -322
- package/dist/chunks/chunk-F6KTJYGV.js.map +0 -7
- package/dist/chunks/chunk-FZITJSSS.js.map +0 -7
- package/dist/chunks/chunk-GSYYA5GX.js +0 -77
- package/dist/chunks/chunk-GSYYA5GX.js.map +0 -7
- package/dist/chunks/chunk-HIZMY3EP.js.map +0 -7
- package/dist/chunks/chunk-I2WEGYA7.js +0 -424
- package/dist/chunks/chunk-I2WEGYA7.js.map +0 -7
- package/dist/chunks/chunk-JNO3CNLJ.js +0 -464
- package/dist/chunks/chunk-JNO3CNLJ.js.map +0 -7
- package/dist/chunks/chunk-NVRBTSQG.js.map +0 -7
- package/dist/chunks/chunk-Q4OBWKXG.js +0 -1143
- package/dist/chunks/chunk-Q4OBWKXG.js.map +0 -7
- package/dist/chunks/chunk-QTE32Y53.js +0 -3829
- package/dist/chunks/chunk-QTE32Y53.js.map +0 -7
- package/dist/chunks/configService-PRJZF7Y6.js +0 -14
- package/dist/chunks/configService-PRJZF7Y6.js.map +0 -7
- package/dist/chunks/constants-KIQEYMAM.js +0 -46
- package/dist/chunks/constants-KIQEYMAM.js.map +0 -7
- package/dist/chunks/fs-ZI5JEU7V.js.map +0 -7
- package/dist/entries/server-router.js +0 -62
- package/dist/entries/server-router.js.map +0 -7
- package/entries/server-router.tsx +0 -75
- package/lib/server/providers/fileSystemPageProvider.test.ts +0 -84
- package/lib/server/providers/fileSystemPageProvider.ts +0 -179
- package/lib/server/webflow/buildWebflow.ts +0 -623
- package/lib/server/webflow/index.ts +0 -23
- package/lib/server/webflow/nodeToWebflow.test.ts +0 -3102
- package/lib/server/webflow/nodeToWebflow.ts +0 -2372
- package/lib/server/webflow/styleMapper.test.ts +0 -348
- package/lib/server/webflow/styleMapper.ts +0 -687
- package/lib/server/webflow/templateWrapper.ts +0 -49
- package/lib/server/webflow/types.ts +0 -389
- /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission resolver
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that turn a {@link PermissionsPolicy} + a
|
|
5
|
+
* {@link PermissionIdentity} into a queryable {@link ResolvedPermissions}, and
|
|
6
|
+
* answer "can this identity edit this resource?". No IO — the policy is loaded
|
|
7
|
+
* and the identity gathered by the caller. Shared by the studio client (UI
|
|
8
|
+
* gating) and any future server-side mirror.
|
|
9
|
+
*
|
|
10
|
+
* See `./types/permissions.ts` for the data model and the SOFT-enforcement
|
|
11
|
+
* caveat.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
PermissionGrant,
|
|
16
|
+
PermissionIdentity,
|
|
17
|
+
PermissionRole,
|
|
18
|
+
PermissionRule,
|
|
19
|
+
PermissionsPolicy,
|
|
20
|
+
ResolvedPermissions,
|
|
21
|
+
ResourceKind,
|
|
22
|
+
ResourceRef,
|
|
23
|
+
} from './types/permissions';
|
|
24
|
+
|
|
25
|
+
/** Role capability ordering, low → high. */
|
|
26
|
+
const ROLE_RANK: Record<PermissionRole, number> = { viewer: 0, editor: 1, admin: 2 };
|
|
27
|
+
|
|
28
|
+
/** Maps a {@link ResourceKind} to its {@link PermissionRule} key. */
|
|
29
|
+
const RULE_KEY: Record<ResourceKind, keyof PermissionRule> = {
|
|
30
|
+
page: 'pages',
|
|
31
|
+
component: 'components',
|
|
32
|
+
cms: 'cms',
|
|
33
|
+
config: 'config',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The empty (all-kinds-present) allow rule used as a merge seed. */
|
|
37
|
+
function emptyAllow(): Required<PermissionRule> {
|
|
38
|
+
return { pages: [], components: [], cms: [], config: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function higherRole(a: PermissionRole, b: PermissionRole): PermissionRole {
|
|
42
|
+
return ROLE_RANK[b] > ROLE_RANK[a] ? b : a;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A grant's role: explicit, else `editor` when it carries an allow-list, else `viewer`. */
|
|
46
|
+
function grantRole(grant: PermissionGrant): PermissionRole {
|
|
47
|
+
if (grant.role) return grant.role;
|
|
48
|
+
return grant.allow ? 'editor' : 'viewer';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mergeAllow(grants: PermissionGrant[]): Required<PermissionRule> {
|
|
52
|
+
const merged = emptyAllow();
|
|
53
|
+
for (const grant of grants) {
|
|
54
|
+
const allow = grant.allow;
|
|
55
|
+
if (!allow) continue;
|
|
56
|
+
for (const kind of Object.keys(merged) as (keyof PermissionRule)[]) {
|
|
57
|
+
const patterns = allow[kind];
|
|
58
|
+
if (patterns) merged[kind].push(...patterns);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Dedupe each list while preserving order.
|
|
62
|
+
for (const kind of Object.keys(merged) as (keyof PermissionRule)[]) {
|
|
63
|
+
merged[kind] = [...new Set(merged[kind])];
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const REGEX_SPECIALS = /[.+?^${}()|[\]\\]/g;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Glob match for a single permission pattern against a value. `*` matches any
|
|
72
|
+
* run of characters (including `/`); every other character is literal. An exact
|
|
73
|
+
* (wildcard-free) pattern must equal the value.
|
|
74
|
+
*/
|
|
75
|
+
export function matchesPermissionPattern(pattern: string, value: string): boolean {
|
|
76
|
+
if (pattern === value) return true;
|
|
77
|
+
if (!pattern.includes('*')) return false;
|
|
78
|
+
const source = pattern
|
|
79
|
+
.split('*')
|
|
80
|
+
.map((segment) => segment.replace(REGEX_SPECIALS, '\\$&'))
|
|
81
|
+
.join('.*');
|
|
82
|
+
return new RegExp(`^${source}$`).test(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** True if `value` matches any pattern in `patterns`. */
|
|
86
|
+
export function matchesAnyPermissionPattern(patterns: string[] | undefined, value: string): boolean {
|
|
87
|
+
return !!patterns && patterns.some((pattern) => matchesPermissionPattern(pattern, value));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a policy + identity into a capability set.
|
|
92
|
+
*
|
|
93
|
+
* - `policy == null` → an UNMANAGED project: everyone edits everything (status
|
|
94
|
+
* quo). `canManagePolicy` (who may create the first policy) is gated to repo
|
|
95
|
+
* admins, or anyone on a local/unauthenticated run.
|
|
96
|
+
* - With a policy present, an identity not listed in `members`/`teams` gets
|
|
97
|
+
* `defaultRole` (default `viewer`). `login: null` is treated the same way —
|
|
98
|
+
* callers that want to bypass gating for local runs should skip resolution
|
|
99
|
+
* rather than pass a null login.
|
|
100
|
+
* - A GitHub repo admin (`identity.isRepoAdmin`) is ALWAYS an admin here,
|
|
101
|
+
* regardless of the policy (bootstrap safety against lockout).
|
|
102
|
+
*/
|
|
103
|
+
export function resolvePermissions(
|
|
104
|
+
policy: PermissionsPolicy | null | undefined,
|
|
105
|
+
identity: PermissionIdentity,
|
|
106
|
+
): ResolvedPermissions {
|
|
107
|
+
const login = identity.login ?? null;
|
|
108
|
+
const isRepoAdmin = identity.isRepoAdmin === true;
|
|
109
|
+
|
|
110
|
+
if (!policy) {
|
|
111
|
+
return {
|
|
112
|
+
login,
|
|
113
|
+
role: 'admin',
|
|
114
|
+
isAdmin: true,
|
|
115
|
+
canManagePolicy: login == null || isRepoAdmin,
|
|
116
|
+
allow: emptyAllow(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const grants: PermissionGrant[] = [];
|
|
121
|
+
|
|
122
|
+
const lookupLogin = login?.toLowerCase() ?? null;
|
|
123
|
+
if (lookupLogin && policy.members) {
|
|
124
|
+
for (const [key, grant] of Object.entries(policy.members)) {
|
|
125
|
+
if (key.toLowerCase() === lookupLogin) grants.push(grant);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (policy.teams && identity.teams && identity.teams.length > 0) {
|
|
130
|
+
const teamSet = new Set(identity.teams.map((team) => team.toLowerCase()));
|
|
131
|
+
for (const [key, grant] of Object.entries(policy.teams)) {
|
|
132
|
+
if (teamSet.has(key.toLowerCase())) grants.push(grant);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const defaultRole: PermissionRole = policy.defaultRole ?? 'viewer';
|
|
137
|
+
const baseRole =
|
|
138
|
+
grants.length === 0
|
|
139
|
+
? defaultRole
|
|
140
|
+
: grants.reduce<PermissionRole>((acc, g) => higherRole(acc, grantRole(g)), 'viewer');
|
|
141
|
+
|
|
142
|
+
const isAdmin = isRepoAdmin || baseRole === 'admin';
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
login,
|
|
146
|
+
role: isAdmin ? 'admin' : baseRole,
|
|
147
|
+
isAdmin,
|
|
148
|
+
canManagePolicy: isAdmin,
|
|
149
|
+
allow: mergeAllow(grants),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* May this identity edit this resource? Admins edit everything; viewers edit
|
|
155
|
+
* nothing; editors edit only resources whose id matches the merged allow-list
|
|
156
|
+
* for the resource's kind.
|
|
157
|
+
*/
|
|
158
|
+
export function canEdit(resolved: ResolvedPermissions, ref: ResourceRef): boolean {
|
|
159
|
+
if (resolved.isAdmin) return true;
|
|
160
|
+
if (resolved.role === 'viewer') return false;
|
|
161
|
+
return matchesAnyPermissionPattern(resolved.allow[RULE_KEY[ref.kind]], ref.id);
|
|
162
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { test, expect, describe
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
2
|
import { resolveComponentProps, resolvePropsFromDefinition, isRichTextMarker } from './propResolver';
|
|
3
3
|
import type { StructuredComponentDefinition, ComponentNode, I18nConfig, PropDefinition } from './types';
|
|
4
4
|
|
|
@@ -146,7 +146,7 @@ export function resolveComponentProps(options: ResolvePropsOptions): Record<stri
|
|
|
146
146
|
// Wrap rich-text props in marker objects for HTML rendering
|
|
147
147
|
// Must happen AFTER i18n resolution so HTML strings get properly wrapped
|
|
148
148
|
return wrapRichTextProps(result, propDefs);
|
|
149
|
-
} catch (
|
|
149
|
+
} catch (_error) {
|
|
150
150
|
// Fallback to original behavior if validation fails (backward compatibility)
|
|
151
151
|
const resolvedProps: Record<string, unknown> = {};
|
|
152
152
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, beforeEach } from 'bun:test';
|
|
6
6
|
import { ClientNodeTypeRegistry } from './ClientNodeTypeRegistry';
|
|
7
|
-
import { SSRNodeTypeRegistry } from './SSRNodeTypeRegistry';
|
|
8
7
|
import { NodeTypeManager, globalNodeTypeManager } from './NodeTypeManager';
|
|
9
8
|
import { defineNodeType } from './defineNodeType';
|
|
10
9
|
import { registerBuiltInNodeTypes, builtInNodeTypes } from './nodeTypes';
|
|
@@ -42,10 +41,11 @@ describe('BaseNodeTypeRegistry', () => {
|
|
|
42
41
|
describe('registerAll', () => {
|
|
43
42
|
it('should register multiple node type definitions', () => {
|
|
44
43
|
registry.registerAll(builtInNodeTypes);
|
|
45
|
-
expect(registry.size).toBe(
|
|
44
|
+
expect(registry.size).toBe(10); // node, component, slot, embed, link, locale-list, list, island, markdown, custom
|
|
46
45
|
expect(registry.has(NODE_TYPE.NODE)).toBe(true);
|
|
47
46
|
expect(registry.has(NODE_TYPE.COMPONENT)).toBe(true);
|
|
48
47
|
expect(registry.has(NODE_TYPE.EMBED)).toBe(true);
|
|
48
|
+
expect(registry.has(NODE_TYPE.ISLAND)).toBe(true);
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -189,6 +189,8 @@ describe('globalNodeTypeManager', () => {
|
|
|
189
189
|
registerBuiltInNodeTypes();
|
|
190
190
|
expect(globalNodeTypeManager.has(NODE_TYPE.NODE)).toBe(true);
|
|
191
191
|
expect(globalNodeTypeManager.has(NODE_TYPE.EMBED)).toBe(true);
|
|
192
|
-
expect(globalNodeTypeManager.
|
|
192
|
+
expect(globalNodeTypeManager.has(NODE_TYPE.ISLAND)).toBe(true);
|
|
193
|
+
expect(globalNodeTypeManager.has(NODE_TYPE.MARKDOWN)).toBe(true);
|
|
194
|
+
expect(globalNodeTypeManager.getAll().length).toBe(10); // node, component, slot, embed, link, locale-list, list, island, markdown, custom
|
|
193
195
|
});
|
|
194
196
|
});
|
|
@@ -133,8 +133,8 @@ describe('BaseComponentRegistry', () => {
|
|
|
133
133
|
|
|
134
134
|
const all = registry.getAll();
|
|
135
135
|
|
|
136
|
-
expect(all
|
|
137
|
-
expect(all
|
|
136
|
+
expect(all.Button).toEqual(component1);
|
|
137
|
+
expect(all.Card).toEqual(component2);
|
|
138
138
|
expect(Object.keys(all).length).toBe(2);
|
|
139
139
|
});
|
|
140
140
|
|
|
@@ -146,7 +146,7 @@ describe('BaseComponentRegistry', () => {
|
|
|
146
146
|
registry.register('Button', component);
|
|
147
147
|
|
|
148
148
|
const all = registry.getAll();
|
|
149
|
-
delete all
|
|
149
|
+
delete all.Button;
|
|
150
150
|
|
|
151
151
|
expect(registry.has('Button')).toBe(true);
|
|
152
152
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ComponentDefinition } from '../types';
|
|
8
|
+
import { logRuntimeError } from '../errorLogger';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Base registry class with common functionality
|
|
@@ -92,7 +93,11 @@ export abstract class BaseComponentRegistry {
|
|
|
92
93
|
for (const listener of this.listeners) {
|
|
93
94
|
try {
|
|
94
95
|
listener();
|
|
95
|
-
} catch (err) {
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// One throwing subscriber must not stop the rest from being notified, but
|
|
98
|
+
// a swallowed listener error hides real store-update bugs — surface it.
|
|
99
|
+
logRuntimeError('ComponentRegistry.notify', err);
|
|
100
|
+
}
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import type { ReactElement } from 'react';
|
|
7
7
|
import type { ZodType } from 'zod';
|
|
8
8
|
import type { ComponentNode } from '../types/nodes';
|
|
9
|
-
import type { StyleValue } from '../types/styles';
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Tree icon types for node display
|
|
@@ -21,7 +20,8 @@ export type TreeIcon =
|
|
|
21
20
|
| 'ARRAY'
|
|
22
21
|
| 'UNKNOWN'
|
|
23
22
|
| 'FORM'
|
|
24
|
-
| 'LINK'
|
|
23
|
+
| 'LINK'
|
|
24
|
+
| 'CODE';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Node category for grouping in command palette
|
|
@@ -33,7 +33,9 @@ interface TextFieldConfig extends BaseFieldConfig {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
interface SelectFieldConfig extends BaseFieldConfig {
|
|
36
|
-
options
|
|
36
|
+
// Bare strings are value-is-label options; the object form carries a distinct
|
|
37
|
+
// display label (e.g. an empty-value `{ value: '', label: 'None' }` entry).
|
|
38
|
+
options: (string | { value: string | boolean; label: string })[];
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
// ============================================
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Node Type Definition
|
|
3
|
+
*
|
|
4
|
+
* Represents a hand-authored, opaque `.astro` component (under `src/custom/`) that Meno
|
|
5
|
+
* treats as a BLACK BOX: it renders server-side in a real `astro build` / play preview
|
|
6
|
+
* (the meno-astro codec emits `<Fancy … />` + the import), but Meno doesn't model its
|
|
7
|
+
* internals — you can place it, pass props, and slot children, but not edit what's inside.
|
|
8
|
+
* The native-Astro sibling of an island (no framework renderer, no hydration).
|
|
9
|
+
*
|
|
10
|
+
* meno-core can't execute a foreign `.astro` file, so the editor canvas renders a quiet
|
|
11
|
+
* placeholder (its slotted children, or an inline marker) — real rendering is play/build
|
|
12
|
+
* only. The on-disk `.astro` form and the round-trip codec live in `meno-astro`; this file
|
|
13
|
+
* only models the node so it validates, persists, and renders a placeholder.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { createElement as h } from 'react';
|
|
18
|
+
import type { ComponentNode } from '../../types/nodes';
|
|
19
|
+
import { IfConditionSchema } from '../../validation/schemas';
|
|
20
|
+
import { createNodeType } from '../createNodeType';
|
|
21
|
+
import { textField } from '../fieldPresets';
|
|
22
|
+
|
|
23
|
+
// Schema is the SINGLE source of truth
|
|
24
|
+
const CustomNodeSchemaInternal = z
|
|
25
|
+
.object({
|
|
26
|
+
type: z.literal('custom'),
|
|
27
|
+
// Path to the hand-authored .astro component relative to `src/custom/`
|
|
28
|
+
// (e.g. "Fancy.astro", "widgets/Banner.astro").
|
|
29
|
+
src: z.string(),
|
|
30
|
+
props: z.record(z.string(), z.any()).optional(),
|
|
31
|
+
if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
|
|
32
|
+
// Slotted children (rendered server-side into the component's default <slot/>).
|
|
33
|
+
children: z.union([z.array(z.any()), z.string()]).optional(),
|
|
34
|
+
})
|
|
35
|
+
.passthrough();
|
|
36
|
+
|
|
37
|
+
// TypeScript type inferred from schema
|
|
38
|
+
export type CustomNode = z.infer<typeof CustomNodeSchemaInternal>;
|
|
39
|
+
|
|
40
|
+
// Export schema for validation/schemas.ts
|
|
41
|
+
export const CustomNodeSchema = CustomNodeSchemaInternal;
|
|
42
|
+
|
|
43
|
+
/** Basename of the custom source (drops directory + `.astro` extension) for display. */
|
|
44
|
+
function customName(src: string): string {
|
|
45
|
+
const base = src.split('/').pop() ?? src;
|
|
46
|
+
return base.replace(/\.astro$/i, '') || base;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Slotted children, normalized to the shape `buildChildren`/`renderChildren` accept. */
|
|
50
|
+
function customChildren(node: CustomNode): Array<ComponentNode | string> | string | undefined {
|
|
51
|
+
const { children } = node;
|
|
52
|
+
if (Array.isArray(children)) return children.length > 0 ? (children as Array<ComponentNode | string>) : undefined;
|
|
53
|
+
if (typeof children === 'string') return children.length > 0 ? children : undefined;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const CustomNodeType = createNodeType({
|
|
58
|
+
type: 'custom',
|
|
59
|
+
displayName: 'Custom Component',
|
|
60
|
+
category: 'special',
|
|
61
|
+
schema: CustomNodeSchemaInternal,
|
|
62
|
+
|
|
63
|
+
defaultValues: {
|
|
64
|
+
src: '',
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
treeDisplay: {
|
|
68
|
+
icon: 'COMPONENT',
|
|
69
|
+
getLabel: (node) => `Custom: ${customName(node.src) || '?'}`,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
editableFields: [textField({ name: 'src', label: 'Custom .astro file', required: true, placeholder: 'Fancy.astro' })],
|
|
73
|
+
|
|
74
|
+
// meno-core can't run a foreign `.astro` file. Render the slotted children as normal
|
|
75
|
+
// content and fall back to a quiet inline marker when there are none. The wrapper keeps
|
|
76
|
+
// `data-custom` for selection/identification only. Real rendering is play/build only.
|
|
77
|
+
clientRenderer: (node, context) => {
|
|
78
|
+
const children = customChildren(node);
|
|
79
|
+
const inner = children
|
|
80
|
+
? context.buildChildren(children, context.elementPath)
|
|
81
|
+
: h('span', { style: { color: '#9ca3af', fontSize: '13px' } }, customName(node.src) || '?');
|
|
82
|
+
return h('div', { key: context.key, 'data-custom': node.src, style: { display: 'contents' } }, inner);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
ssrRenderer: (node, context) => {
|
|
86
|
+
const children = customChildren(node);
|
|
87
|
+
const inner = children
|
|
88
|
+
? context.renderChildren(children, context)
|
|
89
|
+
: `<span style="color:#9ca3af;font-size:13px">${escapeHtml(customName(node.src) || '?')}</span>`;
|
|
90
|
+
return `<div data-custom="${escapeHtml(node.src)}" style="display:contents">${inner}</div>`;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
capabilities: {
|
|
94
|
+
canHaveChildren: true,
|
|
95
|
+
canHaveStyle: false,
|
|
96
|
+
canHaveAttributes: false,
|
|
97
|
+
requiresProps: ['src'],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function escapeHtml(value: string): string {
|
|
102
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
103
|
+
}
|
|
@@ -51,7 +51,7 @@ export const HtmlNodeType = createNodeType({
|
|
|
51
51
|
},
|
|
52
52
|
|
|
53
53
|
clientRenderer: (node, context) => {
|
|
54
|
-
const props: Record<string,
|
|
54
|
+
const props: Record<string, unknown> = { key: context.key };
|
|
55
55
|
|
|
56
56
|
// Prevent form submission in editor
|
|
57
57
|
if (node.tag === 'form') {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Island Node Type Definition
|
|
3
|
+
*
|
|
4
|
+
* Represents a BYO framework component (React/Preact/Vue/Svelte) rendered as an
|
|
5
|
+
* Astro island. The actual hydration only happens in a real `astro build` / play
|
|
6
|
+
* preview (the meno-astro codec emits `<Counter client:visible … />`). meno-core
|
|
7
|
+
* itself can't run a framework component, so the editor canvas renders a visible
|
|
8
|
+
* placeholder — design-time positioning works, interactivity is play/build only.
|
|
9
|
+
*
|
|
10
|
+
* The on-disk `.astro` form and the round-trip codec live in `meno-astro`; this
|
|
11
|
+
* file only models the node so it validates, persists, and renders a placeholder.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { createElement as h } from 'react';
|
|
16
|
+
import type { ComponentNode } from '../../types/nodes';
|
|
17
|
+
import { IfConditionSchema } from '../../validation/schemas';
|
|
18
|
+
import { createNodeType } from '../createNodeType';
|
|
19
|
+
import { textField, selectField } from '../fieldPresets';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Client (hydration) directive for an island.
|
|
23
|
+
* `directive` maps to Astro's `client:*` directive; `value` carries the media
|
|
24
|
+
* query (`client:media="(max-width: 50em)"`) or the framework name
|
|
25
|
+
* (`client:only="react"`). When omitted entirely, the island is server-rendered
|
|
26
|
+
* with no JS (a valid, zero-hydration Astro island).
|
|
27
|
+
*/
|
|
28
|
+
export const IslandClientDirectiveSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
directive: z.enum(['load', 'idle', 'visible', 'media', 'only']),
|
|
31
|
+
value: z.string().optional(),
|
|
32
|
+
})
|
|
33
|
+
.passthrough();
|
|
34
|
+
|
|
35
|
+
// Schema is the SINGLE source of truth
|
|
36
|
+
const IslandNodeSchemaInternal = z
|
|
37
|
+
.object({
|
|
38
|
+
type: z.literal('island'),
|
|
39
|
+
// Path to the framework component file relative to `src/islands/`
|
|
40
|
+
// (e.g. "Counter.tsx", "widgets/Chart.vue"). The extension derives the
|
|
41
|
+
// framework, which drives the Astro integration that gets provisioned.
|
|
42
|
+
src: z.string(),
|
|
43
|
+
client: IslandClientDirectiveSchema.optional(),
|
|
44
|
+
props: z.record(z.string(), z.any()).optional(),
|
|
45
|
+
if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
|
|
46
|
+
// Slotted children (rendered server-side into the island's <slot/>).
|
|
47
|
+
children: z.union([z.array(z.any()), z.string()]).optional(),
|
|
48
|
+
})
|
|
49
|
+
.passthrough();
|
|
50
|
+
|
|
51
|
+
// TypeScript type inferred from schema
|
|
52
|
+
export type IslandNode = z.infer<typeof IslandNodeSchemaInternal>;
|
|
53
|
+
|
|
54
|
+
// Export schema for validation/schemas.ts
|
|
55
|
+
export const IslandNodeSchema = IslandNodeSchemaInternal;
|
|
56
|
+
|
|
57
|
+
/** Basename of the island source (drops directory + extension) for display. */
|
|
58
|
+
function islandName(src: string): string {
|
|
59
|
+
const base = src.split('/').pop() ?? src;
|
|
60
|
+
return base.replace(/\.(tsx|jsx|vue|svelte)$/i, '') || base;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Slotted children, normalized to the shape `buildChildren`/`renderChildren` accept. */
|
|
64
|
+
function islandChildren(node: IslandNode): Array<ComponentNode | string> | string | undefined {
|
|
65
|
+
const { children } = node;
|
|
66
|
+
if (Array.isArray(children)) return children.length > 0 ? (children as Array<ComponentNode | string>) : undefined;
|
|
67
|
+
if (typeof children === 'string') return children.length > 0 ? children : undefined;
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const IslandNodeType = createNodeType({
|
|
72
|
+
type: 'island',
|
|
73
|
+
displayName: 'Island',
|
|
74
|
+
category: 'special',
|
|
75
|
+
schema: IslandNodeSchemaInternal,
|
|
76
|
+
|
|
77
|
+
defaultValues: {
|
|
78
|
+
src: '',
|
|
79
|
+
client: { directive: 'load' },
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
treeDisplay: {
|
|
83
|
+
icon: 'COMPONENT',
|
|
84
|
+
getLabel: (node) => `Island: ${islandName(node.src) || '?'}`,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
editableFields: [
|
|
88
|
+
textField({ name: 'src', label: 'Island file', required: true, placeholder: 'Counter.tsx' }),
|
|
89
|
+
selectField({
|
|
90
|
+
name: 'client.directive',
|
|
91
|
+
label: 'Hydration',
|
|
92
|
+
// The empty-value entry clears `client` entirely → a server-rendered island
|
|
93
|
+
// with zero JS (a valid Astro pattern); the rest map to `client:<directive>`.
|
|
94
|
+
options: [{ value: '', label: 'None (server-only, no JS)' }, 'load', 'idle', 'visible', 'media', 'only'],
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
|
|
98
|
+
// meno-core can't run a framework component. Instead of a loud highlight box
|
|
99
|
+
// (which made island-heavy pages unreadable), render the island's slotted
|
|
100
|
+
// children as normal content and fall back to a quiet inline marker when there
|
|
101
|
+
// are none. The wrapper keeps `data-island` for selection/identification only.
|
|
102
|
+
// Real hydration is play/build only.
|
|
103
|
+
clientRenderer: (node, context) => {
|
|
104
|
+
const children = islandChildren(node);
|
|
105
|
+
const inner = children
|
|
106
|
+
? context.buildChildren(children, context.elementPath)
|
|
107
|
+
: h('span', { style: { color: '#9ca3af', fontSize: '13px' } }, islandName(node.src) || '?');
|
|
108
|
+
return h('div', { key: context.key, 'data-island': node.src, style: { display: 'contents' } }, inner);
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
ssrRenderer: (node, context) => {
|
|
112
|
+
const children = islandChildren(node);
|
|
113
|
+
const inner = children
|
|
114
|
+
? context.renderChildren(children, context)
|
|
115
|
+
: `<span style="color:#9ca3af;font-size:13px">${escapeHtml(islandName(node.src) || '?')}</span>`;
|
|
116
|
+
return `<div data-island="${escapeHtml(node.src)}" style="display:contents">${inner}</div>`;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
capabilities: {
|
|
120
|
+
canHaveChildren: true,
|
|
121
|
+
canHaveStyle: false,
|
|
122
|
+
canHaveAttributes: false,
|
|
123
|
+
requiresProps: ['src'],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
function escapeHtml(value: string): string {
|
|
128
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
129
|
+
}
|
|
@@ -41,14 +41,38 @@ const ListNodeSchemaInternal = z
|
|
|
41
41
|
* Data source type:
|
|
42
42
|
* - 'prop': Read items from component props (default)
|
|
43
43
|
* - 'collection': Query items from CMS collection
|
|
44
|
+
* - 'remote': Fetch items from a public HTTP/JSON endpoint at build/SSR time
|
|
45
|
+
* - 'sanity': Fetch a Sanity document type via GROQ at build/SSR time (read-only, public dataset)
|
|
46
|
+
* - 'expression': Iterate a verbatim JS expression (`source`) that yields an array — a
|
|
47
|
+
* `.map()` over arbitrary frontmatter data (e.g. an Astro Action / supabase query) the
|
|
48
|
+
* dialect can't model as a prop/collection. The item template (`children` + `itemAs`) IS
|
|
49
|
+
* editable; the source itself is a read-only black box (edit it in the .astro frontmatter).
|
|
50
|
+
* The editor has no data for it, so it renders empty in the design canvas (real Astro play
|
|
51
|
+
* renders it for real at build/SSR).
|
|
44
52
|
*/
|
|
45
|
-
sourceType: z.enum(['prop', 'collection']).default('prop'),
|
|
53
|
+
sourceType: z.enum(['prop', 'collection', 'remote', 'sanity', 'expression']).default('prop'),
|
|
46
54
|
/**
|
|
47
|
-
* Source identifier:
|
|
55
|
+
* Source identifier (sourceType 'prop'/'collection'/'expression'; 'remote' uses `url`):
|
|
48
56
|
* - For sourceType 'prop': Prop name (e.g., "items") or template expression (e.g., "{{category.items}}")
|
|
49
57
|
* - For sourceType 'collection': Collection name (e.g., "posts", "authors")
|
|
58
|
+
* - For sourceType 'expression': the verbatim JS expression iterated by `.map()` (e.g. "notes?")
|
|
50
59
|
*/
|
|
51
|
-
source: z.string(),
|
|
60
|
+
source: z.string().optional(),
|
|
61
|
+
|
|
62
|
+
// Remote-only options (sourceType: 'remote')
|
|
63
|
+
/** HTTP(S) endpoint returning JSON, fetched at build/SSR time (e.g. a public REST API). */
|
|
64
|
+
url: z.string().optional(),
|
|
65
|
+
/**
|
|
66
|
+
* Dot-path into the JSON response to reach the items array (e.g. "data.items").
|
|
67
|
+
* Empty/absent = the response body is itself the array.
|
|
68
|
+
*/
|
|
69
|
+
path: z.string().optional(),
|
|
70
|
+
|
|
71
|
+
// Sanity-only options (sourceType: 'sanity')
|
|
72
|
+
/** Sanity document `_type` to fetch (`*[_type == "<documentType>"]`). projectId/dataset come from project config. */
|
|
73
|
+
documentType: z.string().optional(),
|
|
74
|
+
/** Field used as the URL slug (for synthesizing item links); defaults to 'slug'. */
|
|
75
|
+
slugField: z.string().optional(),
|
|
52
76
|
label: z.string().optional(), // Custom label displayed in structure tree
|
|
53
77
|
if: IfConditionSchema.optional(), // Conditional rendering - skip node when false
|
|
54
78
|
/**
|
|
@@ -111,8 +135,19 @@ export const ListNodeType = createNodeType({
|
|
|
111
135
|
getLabel: (node) => {
|
|
112
136
|
const listNode = node as ListNode;
|
|
113
137
|
const sourceType = listNode.sourceType || 'prop';
|
|
114
|
-
const prefix =
|
|
115
|
-
|
|
138
|
+
const prefix =
|
|
139
|
+
sourceType === 'collection'
|
|
140
|
+
? 'CMS List'
|
|
141
|
+
: sourceType === 'remote'
|
|
142
|
+
? 'Remote List'
|
|
143
|
+
: sourceType === 'sanity'
|
|
144
|
+
? 'Sanity List'
|
|
145
|
+
: sourceType === 'expression'
|
|
146
|
+
? 'Loop'
|
|
147
|
+
: 'List';
|
|
148
|
+
const src =
|
|
149
|
+
sourceType === 'remote' ? listNode.url : sourceType === 'sanity' ? listNode.documentType : listNode.source;
|
|
150
|
+
return src ? `${prefix}: ${src}` : prefix;
|
|
116
151
|
},
|
|
117
152
|
},
|
|
118
153
|
|
|
@@ -132,7 +167,8 @@ export const ListNodeType = createNodeType({
|
|
|
132
167
|
key: context.key,
|
|
133
168
|
'data-list': 'true',
|
|
134
169
|
'data-source-type': sourceType,
|
|
135
|
-
'data-source':
|
|
170
|
+
'data-source':
|
|
171
|
+
sourceType === 'remote' ? listNode.url : sourceType === 'sanity' ? listNode.documentType : listNode.source,
|
|
136
172
|
style: {
|
|
137
173
|
padding: '8px 12px',
|
|
138
174
|
background: bgColor,
|
|
@@ -162,7 +198,7 @@ export const ListNodeType = createNodeType({
|
|
|
162
198
|
label: 'Source Type',
|
|
163
199
|
type: 'select',
|
|
164
200
|
required: false,
|
|
165
|
-
options: ['prop', 'collection'],
|
|
201
|
+
options: ['prop', 'collection', 'remote', 'sanity'],
|
|
166
202
|
},
|
|
167
203
|
{
|
|
168
204
|
name: 'source',
|