meno-core 1.0.53 → 1.0.54
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/.claude/settings.local.json +1 -3
- package/bin/cli.ts +48 -57
- package/build-astro.ts +131 -113
- package/build-next.ts +109 -96
- package/build-static.test.ts +39 -10
- package/build-static.ts +120 -122
- package/dist/bin/cli.js +34 -38
- package/dist/bin/cli.js.map +2 -2
- package/dist/build-static.js +12 -11
- package/dist/chunks/chunk-2AR55GYH.js +42 -0
- package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
- package/dist/chunks/{chunk-IGYR22T6.js → chunk-2FN4UOVO.js} +512 -239
- package/dist/chunks/chunk-2FN4UOVO.js.map +7 -0
- package/dist/chunks/chunk-3XER4E5W.js +168 -0
- package/dist/chunks/chunk-3XER4E5W.js.map +7 -0
- package/dist/chunks/{chunk-X754AHS5.js → chunk-5ETZFREW.js} +9 -12
- package/dist/chunks/chunk-5ETZFREW.js.map +7 -0
- package/dist/chunks/{chunk-2MHDV5BF.js → chunk-7E4IF5L7.js} +15 -21
- package/dist/chunks/chunk-7E4IF5L7.js.map +7 -0
- package/dist/chunks/{chunk-JGWFTO6P.js → chunk-7HWQUVTU.js} +1578 -1349
- package/dist/chunks/chunk-7HWQUVTU.js.map +7 -0
- package/dist/chunks/{chunk-O3NAGJP4.js → chunk-AE3QK5QW.js} +110 -21
- package/dist/chunks/chunk-AE3QK5QW.js.map +7 -0
- package/dist/chunks/{chunk-GZHGVVW3.js → chunk-F6KTJYGV.js} +7 -7
- package/dist/chunks/chunk-F6KTJYGV.js.map +7 -0
- package/dist/chunks/{chunk-WQFG7PAH.js → chunk-FZITJSSS.js} +2 -6
- package/dist/chunks/chunk-FZITJSSS.js.map +7 -0
- package/dist/chunks/{chunk-QB2LNO4W.js → chunk-GSYYA5GX.js} +2 -2
- package/dist/chunks/chunk-GSYYA5GX.js.map +7 -0
- package/dist/chunks/{chunk-YBLHKYFF.js → chunk-HIZMY3EP.js} +1 -1
- package/dist/chunks/chunk-HIZMY3EP.js.map +7 -0
- package/dist/chunks/{chunk-AZQYF6KE.js → chunk-I2WEGYA7.js} +41 -176
- package/dist/chunks/chunk-I2WEGYA7.js.map +7 -0
- package/dist/chunks/{chunk-I7YIGZXT.js → chunk-JNO3CNLJ.js} +6 -9
- package/dist/chunks/chunk-JNO3CNLJ.js.map +7 -0
- package/dist/chunks/{chunk-UB44F4Z2.js → chunk-NVRBTSQG.js} +2 -4
- package/dist/chunks/chunk-NVRBTSQG.js.map +7 -0
- package/dist/chunks/{chunk-JGP5A3Y5.js → chunk-Q4OBWKXG.js} +40 -33
- package/dist/chunks/chunk-Q4OBWKXG.js.map +7 -0
- package/dist/chunks/{chunk-R6XHAFBF.js → chunk-QTE32Y53.js} +250 -242
- package/dist/chunks/chunk-QTE32Y53.js.map +7 -0
- package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-STDY3OVM.js} +214 -85
- package/dist/chunks/chunk-STDY3OVM.js.map +7 -0
- package/dist/chunks/configService-PRJZF7Y6.js +14 -0
- package/dist/chunks/{constants-STK2YBIW.js → constants-KIQEYMAM.js} +2 -2
- package/dist/chunks/{fs-JGINUXGL.js → fs-ZI5JEU7V.js} +2 -2
- package/dist/entries/server-router.js +14 -19
- package/dist/entries/server-router.js.map +2 -2
- package/dist/lib/client/index.js +638 -332
- package/dist/lib/client/index.js.map +2 -2
- package/dist/lib/server/index.js +177 -235
- package/dist/lib/server/index.js.map +2 -2
- package/dist/lib/shared/index.js +80 -44
- package/dist/lib/shared/index.js.map +2 -2
- package/dist/lib/shared/richtext/index.js +1 -1
- package/dist/lib/test-utils/index.js +38 -60
- package/dist/lib/test-utils/index.js.map +2 -2
- package/entries/client-router.tsx +10 -8
- package/entries/server-router.tsx +1 -7
- package/lib/client/ClientInitializer.ts +8 -8
- package/lib/client/ErrorBoundary.test.tsx +146 -143
- package/lib/client/ErrorBoundary.tsx +175 -127
- package/lib/client/componentRegistry.test.ts +96 -108
- package/lib/client/componentRegistry.ts +1 -2
- package/lib/client/contexts/ThemeContext.tsx +3 -2
- package/lib/client/core/ComponentBuilder.test.ts +513 -560
- package/lib/client/core/ComponentBuilder.ts +318 -146
- package/lib/client/core/ComponentRenderer.test.tsx +1 -2
- package/lib/client/core/ComponentRenderer.tsx +46 -33
- package/lib/client/core/builders/embedBuilder.ts +241 -53
- package/lib/client/core/builders/linkBuilder.ts +71 -44
- package/lib/client/core/builders/linkNodeBuilder.ts +78 -53
- package/lib/client/core/builders/listBuilder.ts +115 -86
- package/lib/client/core/builders/localeListBuilder.ts +95 -60
- package/lib/client/core/builders/types.ts +5 -5
- package/lib/client/core/cmsTemplateProcessor.ts +7 -7
- package/lib/client/elementRegistry.ts +3 -3
- package/lib/client/fontFamiliesService.test.ts +2 -10
- package/lib/client/fontFamiliesService.ts +3 -3
- package/lib/client/hmr/HMRManager.tsx +8 -0
- package/lib/client/hmrCssReload.ts +14 -8
- package/lib/client/hmrWebSocket.ts +9 -14
- package/lib/client/hooks/useColorVariables.test.ts +21 -21
- package/lib/client/hooks/useColorVariables.ts +12 -10
- package/lib/client/hooks/usePropertyAutocomplete.ts +3 -5
- package/lib/client/hooks/useVariables.ts +4 -4
- package/lib/client/hydration/HydrationUtils.test.ts +24 -25
- package/lib/client/hydration/HydrationUtils.ts +3 -4
- package/lib/client/i18nConfigService.test.ts +2 -7
- package/lib/client/i18nConfigService.ts +2 -2
- package/lib/client/meno-filter/MenoFilter.test.ts +19 -21
- package/lib/client/meno-filter/MenoFilter.ts +5 -9
- package/lib/client/meno-filter/bindings.ts +15 -40
- package/lib/client/meno-filter/init.ts +1 -1
- package/lib/client/meno-filter/renderer.ts +23 -29
- package/lib/client/meno-filter/script.generated.ts +1 -3
- package/lib/client/meno-filter/ui.ts +3 -5
- package/lib/client/meno-filter/updates.ts +15 -21
- package/lib/client/navigation.test.ts +159 -159
- package/lib/client/navigation.ts +0 -1
- package/lib/client/responsiveStyleResolver.test.ts +230 -228
- package/lib/client/responsiveStyleResolver.ts +13 -16
- package/lib/client/routing/RouteLoader.test.ts +23 -24
- package/lib/client/routing/RouteLoader.ts +22 -35
- package/lib/client/routing/Router.tsx +31 -3
- package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
- package/lib/client/scripts/ScriptExecutor.ts +15 -33
- package/lib/client/services/PrefetchService.test.ts +2 -2
- package/lib/client/services/PrefetchService.ts +10 -24
- package/lib/client/styleProcessor.test.ts +9 -9
- package/lib/client/styleProcessor.ts +18 -15
- package/lib/client/styles/StyleInjector.test.ts +122 -115
- package/lib/client/styles/StyleInjector.ts +9 -6
- package/lib/client/styles/UtilityClassCollector.ts +19 -26
- package/lib/client/styles/cspNonce.test.ts +2 -5
- package/lib/client/templateEngine.test.ts +554 -482
- package/lib/client/templateEngine.ts +200 -64
- package/lib/client/theme.ts +0 -1
- package/lib/client/utils/toast.ts +0 -1
- package/lib/server/__integration__/api-routes.test.ts +8 -4
- package/lib/server/__integration__/cms-integration.test.ts +1 -4
- package/lib/server/__integration__/server-lifecycle.test.ts +2 -5
- package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
- package/lib/server/__integration__/static-assets.test.ts +1 -1
- package/lib/server/__integration__/test-helpers.ts +84 -70
- package/lib/server/ab/generateFunctions.ts +12 -10
- package/lib/server/astro/cmsPageEmitter.ts +23 -38
- package/lib/server/astro/componentEmitter.ts +25 -36
- package/lib/server/astro/cssCollector.ts +10 -26
- package/lib/server/astro/nodeToAstro.test.ts +1750 -30
- package/lib/server/astro/nodeToAstro.ts +198 -187
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +105 -109
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +4 -12
- package/lib/server/astro/pageEmitter.ts +9 -13
- package/lib/server/astro/tailwindMapper.test.ts +10 -37
- package/lib/server/astro/tailwindMapper.ts +33 -40
- package/lib/server/astro/templateTransformer.ts +14 -17
- package/lib/server/createServer.ts +5 -17
- package/lib/server/cssGenerator.test.ts +35 -44
- package/lib/server/cssGenerator.ts +6 -17
- package/lib/server/fileWatcher.test.ts +124 -10
- package/lib/server/fileWatcher.ts +124 -118
- package/lib/server/index.ts +7 -1
- package/lib/server/jsonLoader.test.ts +39 -2
- package/lib/server/jsonLoader.ts +33 -31
- package/lib/server/middleware/cors.test.ts +20 -20
- package/lib/server/middleware/cors.ts +28 -4
- package/lib/server/middleware/errorHandler.test.ts +5 -3
- package/lib/server/middleware/errorHandler.ts +3 -8
- package/lib/server/middleware/index.ts +0 -1
- package/lib/server/middleware/logger.test.ts +7 -5
- package/lib/server/middleware/logger.ts +10 -22
- package/lib/server/pageCache.test.ts +76 -77
- package/lib/server/pageCache.ts +0 -1
- package/lib/server/projectContext.ts +4 -3
- package/lib/server/providers/fileSystemCMSProvider.test.ts +124 -95
- package/lib/server/providers/fileSystemCMSProvider.ts +35 -20
- package/lib/server/providers/fileSystemPageProvider.test.ts +84 -0
- package/lib/server/providers/fileSystemPageProvider.ts +31 -12
- package/lib/server/routes/api/cms.test.ts +26 -14
- package/lib/server/routes/api/cms.ts +9 -14
- package/lib/server/routes/api/components.ts +30 -34
- package/lib/server/routes/api/config.ts +0 -1
- package/lib/server/routes/api/core-routes.ts +47 -74
- package/lib/server/routes/api/functions.ts +8 -16
- package/lib/server/routes/api/index.ts +3 -6
- package/lib/server/routes/api/pages.ts +8 -11
- package/lib/server/routes/api/shared.test.ts +1 -1
- package/lib/server/routes/api/shared.ts +12 -9
- package/lib/server/routes/api/variables.test.ts +1 -3
- package/lib/server/routes/api/variables.ts +1 -1
- package/lib/server/routes/index.ts +23 -26
- package/lib/server/routes/pages.ts +34 -29
- package/lib/server/routes/static.ts +16 -4
- package/lib/server/runtime/bundler.ts +47 -32
- package/lib/server/runtime/fs.ts +3 -13
- package/lib/server/runtime/httpServer.ts +5 -9
- package/lib/server/services/ColorService.ts +32 -27
- package/lib/server/services/EnumService.test.ts +2 -6
- package/lib/server/services/EnumService.ts +7 -2
- package/lib/server/services/VariableService.test.ts +1 -5
- package/lib/server/services/VariableService.ts +6 -1
- package/lib/server/services/cmsService.test.ts +116 -78
- package/lib/server/services/cmsService.ts +24 -54
- package/lib/server/services/componentService.test.ts +122 -36
- package/lib/server/services/componentService.ts +54 -38
- package/lib/server/services/configService.test.ts +9 -31
- package/lib/server/services/configService.ts +20 -27
- package/lib/server/services/fileWatcherService.ts +40 -6
- package/lib/server/services/index.ts +0 -1
- package/lib/server/services/pageService.test.ts +1 -3
- package/lib/server/services/pageService.ts +14 -13
- package/lib/server/ssr/attributeBuilder.ts +16 -6
- package/lib/server/ssr/buildErrorOverlay.ts +11 -12
- package/lib/server/ssr/clientDataInjector.ts +7 -21
- package/lib/server/ssr/cmsSSRProcessor.ts +3 -6
- package/lib/server/ssr/cssCollector.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +1 -1
- package/lib/server/ssr/errorOverlay.ts +3 -9
- package/lib/server/ssr/htmlGenerator.nonce.test.ts +3 -9
- package/lib/server/ssr/htmlGenerator.test.ts +120 -43
- package/lib/server/ssr/htmlGenerator.ts +120 -85
- package/lib/server/ssr/imageMetadata.test.ts +3 -1
- package/lib/server/ssr/imageMetadata.ts +25 -19
- package/lib/server/ssr/jsCollector.test.ts +3 -13
- package/lib/server/ssr/jsCollector.ts +3 -8
- package/lib/server/ssr/liveReloadIntegration.test.ts +69 -28
- package/lib/server/ssr/metaTagGenerator.ts +2 -2
- package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
- package/lib/server/ssr/ssrRenderer.test.ts +196 -246
- package/lib/server/ssr/ssrRenderer.ts +609 -225
- package/lib/server/ssrRenderer.test.ts +1044 -950
- package/lib/server/utils/jsonLineMapper.test.ts +28 -28
- package/lib/server/utils/jsonLineMapper.ts +1 -1
- package/lib/server/validateStyleCoverage.ts +18 -20
- package/lib/server/webflow/buildWebflow.ts +41 -53
- package/lib/server/webflow/nodeToWebflow.test.ts +150 -218
- package/lib/server/webflow/nodeToWebflow.ts +195 -258
- package/lib/server/webflow/styleMapper.test.ts +15 -56
- package/lib/server/webflow/styleMapper.ts +33 -41
- package/lib/server/webflow/types.ts +1 -8
- package/lib/server/websocketManager.ts +16 -19
- package/lib/shared/attributeNodeUtils.test.ts +15 -15
- package/lib/shared/attributeNodeUtils.ts +5 -12
- package/lib/shared/breakpoints.ts +4 -11
- package/lib/shared/cmsQueryParser.test.ts +50 -42
- package/lib/shared/cmsQueryParser.ts +49 -31
- package/lib/shared/colorVariableUtils.test.ts +5 -5
- package/lib/shared/colorVariableUtils.ts +3 -8
- package/lib/shared/componentRefs.ts +1 -5
- package/lib/shared/constants.test.ts +3 -3
- package/lib/shared/constants.ts +4 -8
- package/lib/shared/cssGeneration.test.ts +262 -144
- package/lib/shared/cssGeneration.ts +189 -516
- package/lib/shared/cssNamedColors.ts +152 -30
- package/lib/shared/cssProperties.test.ts +5 -6
- package/lib/shared/cssProperties.ts +296 -112
- package/lib/shared/elementClassName.test.ts +109 -109
- package/lib/shared/elementClassName.ts +1 -1
- package/lib/shared/elementUtils.ts +12 -16
- package/lib/shared/errorLogger.ts +2 -10
- package/lib/shared/errors.test.ts +2 -13
- package/lib/shared/errors.ts +2 -8
- package/lib/shared/expressionEvaluator.test.ts +119 -0
- package/lib/shared/expressionEvaluator.ts +43 -24
- package/lib/shared/fontLoader.test.ts +19 -5
- package/lib/shared/friendlyError.ts +2 -3
- package/lib/shared/gradientUtils.test.ts +1 -5
- package/lib/shared/gradientUtils.ts +2 -6
- package/lib/shared/hrefRefs.ts +2 -10
- package/lib/shared/i18n.test.ts +1 -1
- package/lib/shared/i18n.ts +13 -34
- package/lib/shared/index.ts +4 -0
- package/lib/shared/inlineSvgStyleRules.ts +2 -4
- package/lib/shared/interactiveStyleMappings.test.ts +11 -33
- package/lib/shared/interactiveStyleMappings.ts +9 -16
- package/lib/shared/interactiveStyles.test.ts +165 -188
- package/lib/shared/interfaces/contentProvider.ts +1 -1
- package/lib/shared/itemTemplateUtils.test.ts +6 -12
- package/lib/shared/itemTemplateUtils.ts +19 -35
- package/lib/shared/jsonRepair.ts +8 -2
- package/lib/shared/libraryLoader.test.ts +15 -49
- package/lib/shared/libraryLoader.ts +7 -22
- package/lib/shared/netlifyLocale404.test.ts +179 -0
- package/lib/shared/netlifyLocale404.ts +110 -0
- package/lib/shared/nodeUtils.test.ts +24 -16
- package/lib/shared/nodeUtils.ts +49 -19
- package/lib/shared/pathArrayUtils.test.ts +1 -2
- package/lib/shared/pathArrayUtils.ts +1 -1
- package/lib/shared/pathSecurity.ts +1 -1
- package/lib/shared/pathUtils.test.ts +4 -6
- package/lib/shared/pathUtils.ts +42 -48
- package/lib/shared/paths/Path.test.ts +2 -2
- package/lib/shared/paths/Path.ts +0 -1
- package/lib/shared/paths/PathConverter.test.ts +1 -1
- package/lib/shared/paths/PathConverter.ts +14 -17
- package/lib/shared/paths/PathUtils.ts +9 -10
- package/lib/shared/paths/PathValidator.test.ts +2 -15
- package/lib/shared/paths/PathValidator.ts +11 -9
- package/lib/shared/paths/index.ts +1 -2
- package/lib/shared/propResolver.test.ts +240 -244
- package/lib/shared/propResolver.ts +14 -25
- package/lib/shared/pxToRem.test.ts +7 -6
- package/lib/shared/pxToRem.ts +2 -5
- package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +9 -5
- package/lib/shared/registry/ClientRegistry.ts +0 -1
- package/lib/shared/registry/ComponentRegistry.test.ts +43 -29
- package/lib/shared/registry/ComponentRegistry.ts +9 -11
- package/lib/shared/registry/NodeTypeDefinition.ts +15 -8
- package/lib/shared/registry/RegistryManager.ts +1 -2
- package/lib/shared/registry/SSRRegistry.ts +0 -1
- package/lib/shared/registry/createNodeType.ts +7 -9
- package/lib/shared/registry/defineNodeType.ts +2 -6
- package/lib/shared/registry/index.ts +0 -1
- package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
- package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
- package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +47 -18
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
- package/lib/shared/registry/nodeTypes/ListNodeType.ts +78 -74
- package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
- package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -7
- package/lib/shared/registry/nodeTypes/index.ts +10 -2
- package/lib/shared/responsiveScaling.test.ts +15 -31
- package/lib/shared/responsiveScaling.ts +55 -37
- package/lib/shared/responsiveStyleUtils.ts +11 -13
- package/lib/shared/richtext/htmlToTiptap.test.ts +23 -14
- package/lib/shared/richtext/htmlToTiptap.ts +1 -3
- package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
- package/lib/shared/richtext/types.ts +1 -8
- package/lib/shared/slugTranslator.test.ts +13 -13
- package/lib/shared/slugTranslator.ts +12 -16
- package/lib/shared/slugify.ts +9 -15
- package/lib/shared/styleNodeUtils.test.ts +8 -8
- package/lib/shared/styleNodeUtils.ts +5 -10
- package/lib/shared/styleUtils.test.ts +87 -61
- package/lib/shared/styleUtils.ts +5 -6
- package/lib/shared/themeDefaults.test.ts +11 -11
- package/lib/shared/themeDefaults.ts +3 -4
- package/lib/shared/tree/PathBuilder.test.ts +62 -138
- package/lib/shared/tree/PathBuilder.ts +49 -39
- package/lib/shared/treePathUtils.test.ts +2 -10
- package/lib/shared/treePathUtils.ts +54 -59
- package/lib/shared/types/api.ts +1 -2
- package/lib/shared/types/cms.ts +25 -21
- package/lib/shared/types/comment.ts +49 -16
- package/lib/shared/types/components.ts +27 -25
- package/lib/shared/types/errors.test.ts +1 -6
- package/lib/shared/types/errors.ts +3 -7
- package/lib/shared/types/experiments.ts +28 -28
- package/lib/shared/types/index.ts +2 -2
- package/lib/shared/types/styles.ts +0 -1
- package/lib/shared/types/variables.test.ts +4 -13
- package/lib/shared/types/variables.ts +48 -27
- package/lib/shared/types.ts +1 -2
- package/lib/shared/utilityClassConfig.ts +648 -321
- package/lib/shared/utilityClassMapper.test.ts +203 -92
- package/lib/shared/utilityClassMapper.ts +188 -246
- package/lib/shared/utilityClassNames.ts +326 -0
- package/lib/shared/utils.test.ts +2 -10
- package/lib/shared/utils.ts +19 -10
- package/lib/shared/validation/cmsValidators.ts +2 -1
- package/lib/shared/validation/commentValidators.test.ts +53 -0
- package/lib/shared/validation/commentValidators.ts +12 -1
- package/lib/shared/validation/propValidator.test.ts +18 -20
- package/lib/shared/validation/propValidator.ts +12 -17
- package/lib/shared/validation/schemas.test.ts +24 -33
- package/lib/shared/validation/schemas.ts +469 -344
- package/lib/shared/validation/validators.test.ts +1 -6
- package/lib/shared/validation/validators.ts +89 -68
- package/lib/shared/viewportUnits.integration.test.ts +14 -10
- package/lib/shared/viewportUnits.test.ts +11 -23
- package/lib/test-utils/dom-setup.ts +1 -1
- package/lib/test-utils/factories/ConsoleMockFactory.ts +3 -7
- package/lib/test-utils/factories/DomMockFactory.ts +7 -19
- package/lib/test-utils/factories/EventMockFactory.ts +7 -13
- package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
- package/lib/test-utils/factories/ServerMockFactory.ts +5 -9
- package/lib/test-utils/factories/StoreMockFactory.ts +14 -25
- package/lib/test-utils/fixtures.ts +45 -45
- package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
- package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
- package/lib/test-utils/helpers.ts +1 -5
- package/lib/test-utils/index.ts +0 -4
- package/lib/test-utils/mockFactories.ts +12 -18
- package/lib/test-utils/mocks.ts +4 -2
- package/package.json +1 -1
- package/scripts/build-meno-filter.ts +1 -4
- package/vite.config.ts +4 -4
- package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
- package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
- package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
- package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
- package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
- package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
- package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
- package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
- package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
- package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
- package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
- package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
- package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
- package/dist/chunks/chunk-X754AHS5.js.map +0 -7
- package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
- package/dist/chunks/configService-R3OGU2UD.js +0 -13
- /package/dist/chunks/{configService-R3OGU2UD.js.map → configService-PRJZF7Y6.js.map} +0 -0
- /package/dist/chunks/{constants-STK2YBIW.js.map → constants-KIQEYMAM.js.map} +0 -0
- /package/dist/chunks/{fs-JGINUXGL.js.map → fs-ZI5JEU7V.js.map} +0 -0
|
@@ -8,7 +8,12 @@ import { join } from 'path';
|
|
|
8
8
|
import type { CMSProvider, CMSSchemaInfo } from '../../shared/interfaces/contentProvider';
|
|
9
9
|
import type { CMSSchema, CMSItem } from '../../shared/types';
|
|
10
10
|
import { validateCMSItem, validateCMSDraftItem } from '../../shared/validation/validators';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
isSafePathSegment,
|
|
13
|
+
isValidIdentifier,
|
|
14
|
+
isReservedDraftFilename,
|
|
15
|
+
CMS_DRAFT_SUFFIX,
|
|
16
|
+
} from '../../shared/pathSecurity';
|
|
12
17
|
import { readJsonFile, fileExists } from '../runtime';
|
|
13
18
|
|
|
14
19
|
const DRAFT_FILE_SUFFIX = `${CMS_DRAFT_SUFFIX}.json`;
|
|
@@ -54,7 +59,9 @@ function normalizeItem(content: unknown, filename: string, isDraft = false): CMS
|
|
|
54
59
|
* `_filename` differs from `_id` keep the field on disk so the on-disk
|
|
55
60
|
* file-routing path can still find them.
|
|
56
61
|
*/
|
|
57
|
-
function stripTransient<T extends Record<string, unknown>>(
|
|
62
|
+
function stripTransient<T extends Record<string, unknown>>(
|
|
63
|
+
item: T,
|
|
64
|
+
): Omit<T, '_slug' | '_isDraft' | '_hasDraft' | '_url'> {
|
|
58
65
|
const { _slug, _isDraft, _hasDraft, _url, _filename, ...rest } = item;
|
|
59
66
|
const out = rest as Omit<T, '_slug' | '_isDraft' | '_hasDraft' | '_url'>;
|
|
60
67
|
if (typeof _filename === 'string' && _filename !== (out as Record<string, unknown>)._id) {
|
|
@@ -72,7 +79,7 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
72
79
|
|
|
73
80
|
constructor(
|
|
74
81
|
private templatesDir: string, // e.g., './templates' - CMS template pages
|
|
75
|
-
private cmsDir: string
|
|
82
|
+
private cmsDir: string, // e.g., './cms' - items stored here
|
|
76
83
|
) {}
|
|
77
84
|
|
|
78
85
|
/**
|
|
@@ -81,7 +88,9 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
81
88
|
*/
|
|
82
89
|
private validateCollection(collection: string): void {
|
|
83
90
|
if (!isValidIdentifier(collection)) {
|
|
84
|
-
throw new Error(
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Invalid collection name: "${collection}". Collection names must contain only letters, numbers, hyphens, and underscores.`,
|
|
93
|
+
);
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -91,7 +100,9 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
91
100
|
*/
|
|
92
101
|
private validateFilename(filename: string): void {
|
|
93
102
|
if (!isSafePathSegment(filename)) {
|
|
94
|
-
throw new Error(
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Invalid filename: "${filename}". Filenames cannot contain path separators or traversal sequences.`,
|
|
105
|
+
);
|
|
95
106
|
}
|
|
96
107
|
if (isReservedDraftFilename(filename)) {
|
|
97
108
|
throw new Error(`Invalid filename: "${filename}". The "${CMS_DRAFT_SUFFIX}" suffix is reserved for draft files.`);
|
|
@@ -114,14 +125,14 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
const files = readdirSync(this.templatesDir);
|
|
117
|
-
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
128
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
|
118
129
|
|
|
119
130
|
const results = await Promise.all(
|
|
120
|
-
jsonFiles.map(async file => {
|
|
131
|
+
jsonFiles.map(async (file) => {
|
|
121
132
|
const filePath = join(this.templatesDir, file);
|
|
122
133
|
const content = await loadJSONFile(filePath);
|
|
123
134
|
return { filePath, content };
|
|
124
|
-
})
|
|
135
|
+
}),
|
|
125
136
|
);
|
|
126
137
|
|
|
127
138
|
for (const { filePath, content } of results) {
|
|
@@ -160,14 +171,14 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
160
171
|
|
|
161
172
|
const files = readdirSync(collectionDir);
|
|
162
173
|
// Published items only — exclude `*.draft.json` files (draft-version siblings)
|
|
163
|
-
const jsonFiles = files.filter(f => f.endsWith('.json') && !f.endsWith(DRAFT_FILE_SUFFIX));
|
|
174
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json') && !f.endsWith(DRAFT_FILE_SUFFIX));
|
|
164
175
|
|
|
165
176
|
const results = await Promise.all(
|
|
166
|
-
jsonFiles.map(async file => {
|
|
177
|
+
jsonFiles.map(async (file) => {
|
|
167
178
|
const filePath = join(collectionDir, file);
|
|
168
179
|
const content = await loadJSONFile(filePath);
|
|
169
180
|
return { file, filePath, content };
|
|
170
|
-
})
|
|
181
|
+
}),
|
|
171
182
|
);
|
|
172
183
|
|
|
173
184
|
const items: CMSItem[] = [];
|
|
@@ -225,7 +236,7 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
225
236
|
*/
|
|
226
237
|
async getItemById(collection: string, id: string): Promise<CMSItem | null> {
|
|
227
238
|
const items = await this.getItems(collection);
|
|
228
|
-
return items.find(item => item._id === id) || null;
|
|
239
|
+
return items.find((item) => item._id === id) || null;
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
/**
|
|
@@ -340,14 +351,14 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
340
351
|
const collectionDir = join(this.cmsDir, collection);
|
|
341
352
|
if (!existsSync(collectionDir)) return [];
|
|
342
353
|
|
|
343
|
-
const files = readdirSync(collectionDir).filter(f => f.endsWith(DRAFT_FILE_SUFFIX));
|
|
354
|
+
const files = readdirSync(collectionDir).filter((f) => f.endsWith(DRAFT_FILE_SUFFIX));
|
|
344
355
|
|
|
345
356
|
const results = await Promise.all(
|
|
346
|
-
files.map(async file => {
|
|
357
|
+
files.map(async (file) => {
|
|
347
358
|
const filePath = join(collectionDir, file);
|
|
348
359
|
const content = await loadJSONFile(filePath);
|
|
349
360
|
return { file, content };
|
|
350
|
-
})
|
|
361
|
+
}),
|
|
351
362
|
);
|
|
352
363
|
|
|
353
364
|
const drafts: CMSItem[] = [];
|
|
@@ -415,7 +426,7 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
415
426
|
// (validateCMSItem) runs on Publish.
|
|
416
427
|
const validation = validateCMSDraftItem(itemData);
|
|
417
428
|
if (!validation.valid) {
|
|
418
|
-
const messages = validation.errors.map(e => `${e.path}: ${e.message}`).join(', ');
|
|
429
|
+
const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join(', ');
|
|
419
430
|
throw new Error(`Invalid draft: ${messages}`);
|
|
420
431
|
}
|
|
421
432
|
|
|
@@ -460,7 +471,7 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
460
471
|
const item = normalizeItem(content, filename);
|
|
461
472
|
const validation = validateCMSItem(item);
|
|
462
473
|
if (!validation.valid) {
|
|
463
|
-
const messages = validation.errors.map(e => `${e.path}: ${e.message}`).join(', ');
|
|
474
|
+
const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join(', ');
|
|
464
475
|
throw new Error(`Cannot publish invalid draft: ${messages}`);
|
|
465
476
|
}
|
|
466
477
|
|
|
@@ -495,7 +506,9 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
495
506
|
*/
|
|
496
507
|
async saveSchema(collectionId: string, pageData: unknown): Promise<void> {
|
|
497
508
|
if (!isValidIdentifier(collectionId)) {
|
|
498
|
-
throw new Error(
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`,
|
|
511
|
+
);
|
|
499
512
|
}
|
|
500
513
|
const { writeFile, mkdir } = await import('fs/promises');
|
|
501
514
|
|
|
@@ -531,7 +544,9 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
531
544
|
*/
|
|
532
545
|
async updateSchema(collectionId: string, updates: Partial<CMSSchema>): Promise<void> {
|
|
533
546
|
if (!isValidIdentifier(collectionId)) {
|
|
534
|
-
throw new Error(
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Invalid collection ID: "${collectionId}". Collection IDs must contain only letters, numbers, hyphens, and underscores.`,
|
|
549
|
+
);
|
|
535
550
|
}
|
|
536
551
|
const { readFile, writeFile } = await import('fs/promises');
|
|
537
552
|
|
|
@@ -552,7 +567,7 @@ export class FileSystemCMSProvider implements CMSProvider {
|
|
|
552
567
|
pageData.meta.cms = {
|
|
553
568
|
...pageData.meta.cms,
|
|
554
569
|
...updates,
|
|
555
|
-
id: collectionId,
|
|
570
|
+
id: collectionId, // ID cannot be changed
|
|
556
571
|
};
|
|
557
572
|
|
|
558
573
|
await writeFile(pageFilePath, JSON.stringify(pageData, null, 2), 'utf-8');
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileSystemPageProvider Tests
|
|
3
|
+
*
|
|
4
|
+
* Focus: normal nested page paths keep working, and page paths that try to escape the
|
|
5
|
+
* pages/templates directory (`..` traversal, NUL bytes) are rejected before any
|
|
6
|
+
* filesystem write/delete can touch an ancestor directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
10
|
+
import { FileSystemPageProvider } from './fileSystemPageProvider';
|
|
11
|
+
import { existsSync, rmSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
const ROOT = join('/tmp', 'page-provider-test-' + Date.now());
|
|
15
|
+
const PAGES_DIR = join(ROOT, 'project', 'pages');
|
|
16
|
+
const TEMPLATES_DIR = join(ROOT, 'project', 'templates');
|
|
17
|
+
// A sentinel directory OUTSIDE pagesDir that a traversal attack would try to reach.
|
|
18
|
+
const OUTSIDE_DIR = join(ROOT, 'project');
|
|
19
|
+
|
|
20
|
+
describe('FileSystemPageProvider', () => {
|
|
21
|
+
let provider: FileSystemPageProvider;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mkdirSync(PAGES_DIR, { recursive: true });
|
|
25
|
+
mkdirSync(TEMPLATES_DIR, { recursive: true });
|
|
26
|
+
provider = new FileSystemPageProvider(PAGES_DIR, TEMPLATES_DIR);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
rmSync(ROOT, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('normal page paths', () => {
|
|
34
|
+
it('saves and reads a top-level page', async () => {
|
|
35
|
+
await provider.save('/about', '{"ok":true}');
|
|
36
|
+
expect(existsSync(join(PAGES_DIR, 'about.json'))).toBe(true);
|
|
37
|
+
expect(await provider.get('/about')).toBe('{"ok":true}');
|
|
38
|
+
expect(await provider.exists('/about')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('saves and reads a nested page, auto-creating folders', async () => {
|
|
42
|
+
await provider.save('/blog/post-1', '{"slug":"post-1"}');
|
|
43
|
+
expect(existsSync(join(PAGES_DIR, 'blog', 'post-1.json'))).toBe(true);
|
|
44
|
+
expect(await provider.get('/blog/post-1')).toBe('{"slug":"post-1"}');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('maps the index route to index.json', async () => {
|
|
48
|
+
await provider.save('/', '{"home":true}');
|
|
49
|
+
expect(existsSync(join(PAGES_DIR, 'index.json'))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('routes template paths to the templates directory', async () => {
|
|
53
|
+
await provider.save('/templates/blog-post', '{"template":true}');
|
|
54
|
+
expect(existsSync(join(TEMPLATES_DIR, 'blog-post.json'))).toBe(true);
|
|
55
|
+
expect(await provider.get('/templates/blog-post')).toBe('{"template":true}');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('path traversal rejection', () => {
|
|
60
|
+
it('rejects save for a path escaping pagesDir', async () => {
|
|
61
|
+
await expect(provider.save('/../../evil', '{"x":1}')).rejects.toThrow(/resolves outside/);
|
|
62
|
+
// Nothing must have been written outside pagesDir.
|
|
63
|
+
expect(existsSync(join(ROOT, 'evil.json'))).toBe(false);
|
|
64
|
+
expect(existsSync(join(OUTSIDE_DIR, 'evil.json'))).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects delete for a path escaping pagesDir', async () => {
|
|
68
|
+
await expect(provider.delete('/../../evil')).rejects.toThrow(/resolves outside/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rejects get/exists for a traversal path', async () => {
|
|
72
|
+
await expect(provider.get('/../../evil')).rejects.toThrow(/resolves outside/);
|
|
73
|
+
await expect(provider.exists('/../../evil')).rejects.toThrow(/resolves outside/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects a template path escaping templatesDir', async () => {
|
|
77
|
+
await expect(provider.save('/templates/../../evil', '{"x":1}')).rejects.toThrow(/resolves outside/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects a path containing a NUL byte', async () => {
|
|
81
|
+
await expect(provider.save('/foo\0bar', '{"x":1}')).rejects.toThrow(/NUL byte/);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, readdirSync, mkdirSync, rmdirSync } from 'fs';
|
|
7
|
-
import { join, dirname } from 'path';
|
|
7
|
+
import { join, dirname, resolve, sep } from 'path';
|
|
8
8
|
import type { PageProvider } from '../../shared/interfaces/contentProvider';
|
|
9
9
|
import { isJSONFile, stripExtension, mapPageNameToPath, mapPathToPageName } from '../../shared/utils/fileUtils';
|
|
10
10
|
import { readTextFile, fileExists } from '../runtime';
|
|
11
|
+
import { logRuntimeError } from '../../shared/errorLogger';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Load JSON file content from disk
|
|
@@ -18,7 +19,8 @@ async function loadJSONFile(filePath: string): Promise<string | null> {
|
|
|
18
19
|
return await readTextFile(filePath);
|
|
19
20
|
}
|
|
20
21
|
return null;
|
|
21
|
-
} catch {
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logRuntimeError('fileSystemPageProvider.loadJSONFile', error, { filePath });
|
|
22
24
|
return null;
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -31,7 +33,7 @@ async function loadJSONFile(filePath: string): Promise<string | null> {
|
|
|
31
33
|
export class FileSystemPageProvider implements PageProvider {
|
|
32
34
|
constructor(
|
|
33
35
|
private pagesDir: string,
|
|
34
|
-
private templatesDir?: string
|
|
36
|
+
private templatesDir?: string,
|
|
35
37
|
) {}
|
|
36
38
|
|
|
37
39
|
async loadAll(): Promise<Map<string, string>> {
|
|
@@ -57,38 +59,55 @@ export class FileSystemPageProvider implements PageProvider {
|
|
|
57
59
|
private async scanDirectory(dir: string, prefix: string, pages: Map<string, string>): Promise<void> {
|
|
58
60
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
59
61
|
|
|
60
|
-
const fileEntries = entries.filter(e => e.isFile() && isJSONFile(e.name));
|
|
61
|
-
const dirEntries = entries.filter(e => e.isDirectory());
|
|
62
|
+
const fileEntries = entries.filter((e) => e.isFile() && isJSONFile(e.name));
|
|
63
|
+
const dirEntries = entries.filter((e) => e.isDirectory());
|
|
62
64
|
|
|
63
65
|
await Promise.all([
|
|
64
|
-
...fileEntries.map(async entry => {
|
|
65
|
-
const pageName = prefix
|
|
66
|
-
? `${prefix}/${stripExtension(entry.name)}`
|
|
67
|
-
: stripExtension(entry.name);
|
|
66
|
+
...fileEntries.map(async (entry) => {
|
|
67
|
+
const pageName = prefix ? `${prefix}/${stripExtension(entry.name)}` : stripExtension(entry.name);
|
|
68
68
|
const content = await loadJSONFile(join(dir, entry.name));
|
|
69
69
|
if (content) {
|
|
70
70
|
const pagePath = mapPageNameToPath(pageName);
|
|
71
71
|
pages.set(pagePath, content);
|
|
72
72
|
}
|
|
73
73
|
}),
|
|
74
|
-
...dirEntries.map(entry => {
|
|
74
|
+
...dirEntries.map((entry) => {
|
|
75
75
|
const subPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
76
76
|
return this.scanDirectory(join(dir, entry.name), subPrefix, pages);
|
|
77
77
|
}),
|
|
78
78
|
]);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Assert that `candidate` resolves inside `baseDir`. Without this a page path
|
|
83
|
+
* like `/../../foo` maps to pageName `../../foo` and would be joined OUTSIDE the
|
|
84
|
+
* pages/templates dir — letting save() write, and delete() (plus its empty-parent
|
|
85
|
+
* rmdir loop) operate on, arbitrary ancestor directories. Throws on escape.
|
|
86
|
+
*/
|
|
87
|
+
private assertWithin(baseDir: string, candidate: string): string {
|
|
88
|
+
const baseResolved = resolve(baseDir);
|
|
89
|
+
const resolved = resolve(candidate);
|
|
90
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + sep)) {
|
|
91
|
+
throw new Error(`Invalid page path: "${candidate}" resolves outside of "${baseDir}"`);
|
|
92
|
+
}
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
/**
|
|
82
97
|
* Resolve a page path to its filesystem file path
|
|
83
98
|
* Routes /templates/* paths to the templates directory
|
|
84
99
|
*/
|
|
85
100
|
private resolveFilePath(path: string): string {
|
|
86
101
|
const pageName = mapPathToPageName(path);
|
|
102
|
+
// Reject NUL bytes outright (fs would throw, but fail fast with a clear error).
|
|
103
|
+
if (pageName.includes('\0')) {
|
|
104
|
+
throw new Error('Invalid page path: contains NUL byte');
|
|
105
|
+
}
|
|
87
106
|
if (this.templatesDir && pageName.startsWith('templates/')) {
|
|
88
107
|
const templateName = pageName.substring('templates/'.length);
|
|
89
|
-
return join(this.templatesDir, `${templateName}.json`);
|
|
108
|
+
return this.assertWithin(this.templatesDir, join(this.templatesDir, `${templateName}.json`));
|
|
90
109
|
}
|
|
91
|
-
return join(this.pagesDir, `${pageName}.json`);
|
|
110
|
+
return this.assertWithin(this.pagesDir, join(this.pagesDir, `${pageName}.json`));
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
/**
|
|
@@ -7,12 +7,7 @@ import { describe, it, expect, beforeEach } from 'bun:test';
|
|
|
7
7
|
import type { CMSProvider, CMSSchemaInfo } from '../../../shared/interfaces/contentProvider';
|
|
8
8
|
import type { CMSItem, CMSSchema } from '../../../shared/types';
|
|
9
9
|
import { CMSService } from '../../services/cmsService';
|
|
10
|
-
import {
|
|
11
|
-
handleCollectionsRoute,
|
|
12
|
-
handleCollectionSchemaRoute,
|
|
13
|
-
handleItemsRoute,
|
|
14
|
-
handleItemRoute,
|
|
15
|
-
} from './cms';
|
|
10
|
+
import { handleCollectionsRoute, handleCollectionSchemaRoute, handleItemsRoute, handleItemRoute } from './cms';
|
|
16
11
|
|
|
17
12
|
// Note: Write handler tests (handleCreateItemRoute, handleUpdateItemRoute, handleDeleteItemRoute)
|
|
18
13
|
// moved to @meno/studio
|
|
@@ -55,7 +50,14 @@ const mockProductSchemaInfo: CMSSchemaInfo = {
|
|
|
55
50
|
|
|
56
51
|
const mockBlogItems: CMSItem[] = [
|
|
57
52
|
{ _id: '1', _slug: 'hello-world', slug: 'hello-world', title: 'Hello World', content: 'First post', featured: true },
|
|
58
|
-
{
|
|
53
|
+
{
|
|
54
|
+
_id: '2',
|
|
55
|
+
_slug: 'getting-started',
|
|
56
|
+
slug: 'getting-started',
|
|
57
|
+
title: 'Getting Started',
|
|
58
|
+
content: 'Guide',
|
|
59
|
+
featured: false,
|
|
60
|
+
},
|
|
59
61
|
];
|
|
60
62
|
|
|
61
63
|
describe('CMS API Routes - Read Operations', () => {
|
|
@@ -64,34 +66,44 @@ describe('CMS API Routes - Read Operations', () => {
|
|
|
64
66
|
|
|
65
67
|
beforeEach(async () => {
|
|
66
68
|
mockProvider = {
|
|
67
|
-
getAllSchemas: async () =>
|
|
68
|
-
[
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
getAllSchemas: async () =>
|
|
70
|
+
new Map([
|
|
71
|
+
['blog-posts', mockSchemaInfo],
|
|
72
|
+
['products', mockProductSchemaInfo],
|
|
73
|
+
]),
|
|
71
74
|
getItems: async (collection: string) => {
|
|
72
75
|
if (collection === 'blog-posts') return [...mockBlogItems];
|
|
73
76
|
return [];
|
|
74
77
|
},
|
|
75
78
|
getItemBySlug: async (collection: string, slug: string) => {
|
|
76
79
|
if (collection === 'blog-posts') {
|
|
77
|
-
return mockBlogItems.find(i => i._slug === slug) || null;
|
|
80
|
+
return mockBlogItems.find((i) => i._slug === slug) || null;
|
|
78
81
|
}
|
|
79
82
|
return null;
|
|
80
83
|
},
|
|
81
84
|
getItemByFilename: async (collection: string, filename: string) => {
|
|
82
85
|
if (collection === 'blog-posts') {
|
|
83
|
-
return mockBlogItems.find(i => i._filename === filename || i._slug === filename) || null;
|
|
86
|
+
return mockBlogItems.find((i) => i._filename === filename || i._slug === filename) || null;
|
|
84
87
|
}
|
|
85
88
|
return null;
|
|
86
89
|
},
|
|
87
90
|
getItemById: async (collection: string, id: string) => {
|
|
88
91
|
if (collection === 'blog-posts') {
|
|
89
|
-
return mockBlogItems.find(i => i._id === id) || null;
|
|
92
|
+
return mockBlogItems.find((i) => i._id === id) || null;
|
|
90
93
|
}
|
|
91
94
|
return null;
|
|
92
95
|
},
|
|
93
96
|
saveItem: async () => {},
|
|
94
97
|
deleteItem: async () => {},
|
|
98
|
+
// Draft-model methods (no drafts exist in this mock)
|
|
99
|
+
getDraft: async () => null,
|
|
100
|
+
getAllDrafts: async () => [],
|
|
101
|
+
hasDraft: async () => false,
|
|
102
|
+
saveDraft: async () => {},
|
|
103
|
+
discardDraft: async () => {},
|
|
104
|
+
publishDraft: async () => {
|
|
105
|
+
throw new Error('No draft exists');
|
|
106
|
+
},
|
|
95
107
|
};
|
|
96
108
|
|
|
97
109
|
cmsService = new CMSService(mockProvider);
|
|
@@ -26,10 +26,7 @@ export function handleCollectionsRoute(cmsService: CMSService): Response {
|
|
|
26
26
|
/**
|
|
27
27
|
* GET /api/cms/collections/:collection - Get schema for specific collection
|
|
28
28
|
*/
|
|
29
|
-
export function handleCollectionSchemaRoute(
|
|
30
|
-
collection: string,
|
|
31
|
-
cmsService: CMSService
|
|
32
|
-
): Response {
|
|
29
|
+
export function handleCollectionSchemaRoute(collection: string, cmsService: CMSService): Response {
|
|
33
30
|
const schema = cmsService.getSchema(collection);
|
|
34
31
|
|
|
35
32
|
if (!schema) {
|
|
@@ -51,7 +48,7 @@ export function handleCollectionSchemaRoute(
|
|
|
51
48
|
export async function handleItemsRoute(
|
|
52
49
|
collection: string,
|
|
53
50
|
cmsService: CMSService,
|
|
54
|
-
searchParams?: URLSearchParams
|
|
51
|
+
searchParams?: URLSearchParams,
|
|
55
52
|
): Promise<Response> {
|
|
56
53
|
// Parse pagination params
|
|
57
54
|
const limit = searchParams?.get('limit') ? parseInt(searchParams.get('limit')!, 10) : undefined;
|
|
@@ -69,7 +66,7 @@ export async function handleItemsRoute(
|
|
|
69
66
|
const schema = cmsService.getSchema(collection);
|
|
70
67
|
const slugField = schema?.slugField || 'slug';
|
|
71
68
|
|
|
72
|
-
filteredItems = allItems.filter(item => {
|
|
69
|
+
filteredItems = allItems.filter((item) => {
|
|
73
70
|
// Search in title, slug, and filename
|
|
74
71
|
const title = extractStringValue(item.title || '').toLowerCase();
|
|
75
72
|
const slug = extractStringValue(item[slugField] || item._slug || '').toLowerCase();
|
|
@@ -95,27 +92,25 @@ export async function handleItemsRoute(
|
|
|
95
92
|
items: itemsWithUrl,
|
|
96
93
|
total: search ? filteredItems.length : total,
|
|
97
94
|
limit,
|
|
98
|
-
offset: offset || 0
|
|
95
|
+
offset: offset || 0,
|
|
99
96
|
});
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
/**
|
|
103
100
|
* GET /api/cms/:collection/:slug - Get specific item by slug
|
|
104
101
|
*/
|
|
105
|
-
export async function handleItemRoute(
|
|
106
|
-
collection: string,
|
|
107
|
-
slug: string,
|
|
108
|
-
cmsService: CMSService
|
|
109
|
-
): Promise<Response> {
|
|
102
|
+
export async function handleItemRoute(collection: string, slug: string, cmsService: CMSService): Promise<Response> {
|
|
110
103
|
const match = await cmsService.matchRoute(
|
|
111
|
-
cmsService.getSchema(collection)?.urlPattern.replace('{{slug}}', slug) || ''
|
|
104
|
+
cmsService.getSchema(collection)?.urlPattern.replace('{{slug}}', slug) || '',
|
|
112
105
|
);
|
|
113
106
|
|
|
114
107
|
// If no match via route, try direct provider access
|
|
115
108
|
if (!match) {
|
|
116
109
|
// For direct item access, we need to check if item exists
|
|
117
110
|
const items = await cmsService.queryItems({ collection });
|
|
118
|
-
const item = items.find(
|
|
111
|
+
const item = items.find(
|
|
112
|
+
(i) => i._slug === slug || i[cmsService.getSchema(collection)?.slugField || 'slug'] === slug,
|
|
113
|
+
);
|
|
119
114
|
|
|
120
115
|
if (!item) {
|
|
121
116
|
return errorResponse('Item not found', 404);
|
|
@@ -16,10 +16,7 @@ import { formatJsonErrorMessage } from '../../../shared/jsonRepair';
|
|
|
16
16
|
* Uses ETag revalidation so repeat page navigations get a 304 when the
|
|
17
17
|
* registry hasn't changed.
|
|
18
18
|
*/
|
|
19
|
-
export function handleComponentsRoute(
|
|
20
|
-
req: Request,
|
|
21
|
-
componentService: ComponentService,
|
|
22
|
-
): Response {
|
|
19
|
+
export function handleComponentsRoute(req: Request, componentService: ComponentService): Response {
|
|
23
20
|
const componentsWithCategories = componentService.getAllComponentsWithCategories();
|
|
24
21
|
|
|
25
22
|
// Transform to API format: { name: { ...definition, _category } }
|
|
@@ -27,7 +24,7 @@ export function handleComponentsRoute(
|
|
|
27
24
|
for (const [name, info] of Object.entries(componentsWithCategories)) {
|
|
28
25
|
result[name] = {
|
|
29
26
|
...info.definition,
|
|
30
|
-
_category: info.category
|
|
27
|
+
_category: info.category,
|
|
31
28
|
};
|
|
32
29
|
}
|
|
33
30
|
|
|
@@ -44,10 +41,7 @@ export function handleComponentsRoute(
|
|
|
44
41
|
* Handle component data API endpoint - GET /api/component-data/:name
|
|
45
42
|
* Validates that component has structure before returning
|
|
46
43
|
*/
|
|
47
|
-
export function handleComponentDataRoute(
|
|
48
|
-
url: URL,
|
|
49
|
-
componentService: ComponentService
|
|
50
|
-
): Response {
|
|
44
|
+
export function handleComponentDataRoute(url: URL, componentService: ComponentService): Response {
|
|
51
45
|
const componentName = url.pathname.replace('/api/component-data/', '');
|
|
52
46
|
const componentDef = componentService.getComponent(componentName);
|
|
53
47
|
|
|
@@ -56,14 +50,12 @@ export function handleComponentDataRoute(
|
|
|
56
50
|
const loadErrors = componentService.getLoadErrors();
|
|
57
51
|
const diagnostic = loadErrors.get(componentName);
|
|
58
52
|
if (diagnostic) {
|
|
59
|
-
const message = formatJsonErrorMessage(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{ status: 422 }
|
|
66
|
-
);
|
|
53
|
+
const message = formatJsonErrorMessage(`${diagnostic.componentName}.json`, {
|
|
54
|
+
message: diagnostic.description,
|
|
55
|
+
line: diagnostic.line,
|
|
56
|
+
column: diagnostic.column,
|
|
57
|
+
});
|
|
58
|
+
return jsonResponse({ error: 'json_parse_error', message, errorCode: 'JSON_PARSE_ERROR' }, { status: 422 });
|
|
67
59
|
}
|
|
68
60
|
return jsonResponse({ error: 'Component not found' }, { status: 404 });
|
|
69
61
|
}
|
|
@@ -73,11 +65,11 @@ export function handleComponentDataRoute(
|
|
|
73
65
|
// Validate that component has structure
|
|
74
66
|
if (!componentService.validateComponentStructure(componentDef)) {
|
|
75
67
|
return jsonResponse(
|
|
76
|
-
{
|
|
68
|
+
{
|
|
77
69
|
error: 'Component structure is missing',
|
|
78
|
-
message: `Component "${componentName}" does not have a valid structure definition. Please add a structure field to the component definition
|
|
79
|
-
},
|
|
80
|
-
{ status: 400 }
|
|
70
|
+
message: `Component "${componentName}" does not have a valid structure definition. Please add a structure field to the component definition.`,
|
|
71
|
+
},
|
|
72
|
+
{ status: 400 },
|
|
81
73
|
);
|
|
82
74
|
}
|
|
83
75
|
|
|
@@ -87,10 +79,7 @@ export function handleComponentDataRoute(
|
|
|
87
79
|
/**
|
|
88
80
|
* Handle component JavaScript API endpoint - GET /api/component-js/:name
|
|
89
81
|
*/
|
|
90
|
-
export async function handleComponentJavaScriptRoute(
|
|
91
|
-
url: URL,
|
|
92
|
-
componentService: ComponentService
|
|
93
|
-
): Promise<Response> {
|
|
82
|
+
export async function handleComponentJavaScriptRoute(url: URL, componentService: ComponentService): Promise<Response> {
|
|
94
83
|
const componentName = url.pathname.replace('/api/component-js/', '');
|
|
95
84
|
const jsContent = await componentService.getComponentJavaScript(componentName);
|
|
96
85
|
const corsHeaders = createCorsHeaders();
|
|
@@ -107,13 +96,18 @@ export async function handleComponentJavaScriptRoute(
|
|
|
107
96
|
* Handle component usage API endpoint - GET /api/component-usage
|
|
108
97
|
* Returns how many times each component is used across all pages and other components
|
|
109
98
|
*/
|
|
110
|
-
export function handleComponentUsageRoute(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function walkTree(
|
|
99
|
+
export function handleComponentUsageRoute(pageService: PageService, componentService: ComponentService): Response {
|
|
100
|
+
const usageMap = new Map<
|
|
101
|
+
string,
|
|
102
|
+
{ count: number; files: Map<string, { name: string; path: string; type: 'page' | 'component' }> }
|
|
103
|
+
>();
|
|
104
|
+
|
|
105
|
+
function walkTree(
|
|
106
|
+
node: ComponentNode | undefined | null,
|
|
107
|
+
fileName: string,
|
|
108
|
+
filePath: string,
|
|
109
|
+
fileType: 'page' | 'component',
|
|
110
|
+
) {
|
|
117
111
|
if (!node || typeof node !== 'object') return;
|
|
118
112
|
if (node.type === 'component' && 'component' in node && (node as any).component) {
|
|
119
113
|
const compName = (node as any).component as string;
|
|
@@ -152,7 +146,10 @@ export function handleComponentUsageRoute(
|
|
|
152
146
|
}
|
|
153
147
|
|
|
154
148
|
// Convert to plain object
|
|
155
|
-
const result: Record<
|
|
149
|
+
const result: Record<
|
|
150
|
+
string,
|
|
151
|
+
{ count: number; files: Array<{ name: string; path: string; type: 'page' | 'component' }> }
|
|
152
|
+
> = {};
|
|
156
153
|
for (const [name, data] of usageMap) {
|
|
157
154
|
result[name] = {
|
|
158
155
|
count: data.count,
|
|
@@ -165,4 +162,3 @@ export function handleComponentUsageRoute(
|
|
|
165
162
|
|
|
166
163
|
// Note: Write handlers (handleSaveComponentRoute, handleSaveComponentJavaScriptRoute,
|
|
167
164
|
// handleSaveComponentCSSRoute, handleComponentCategoryRoute) moved to @meno/studio
|
|
168
|
-
|