@wrikka/create-docs 0.1.0
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/README.md +137 -0
- package/bin/cli.js +408 -0
- package/dist/index.cjs +20 -0
- package/dist/index.d.cts +475 -0
- package/dist/index.d.ts +475 -0
- package/dist/index.js +20 -0
- package/docs/_dir.yml +2 -0
- package/docs/config.md +75 -0
- package/docs/data-sources.md +88 -0
- package/docs/deploy.md +38 -0
- package/docs/features.md +51 -0
- package/docs/getting-started.md +42 -0
- package/docs/index.md +34 -0
- package/docs/markdown.md +154 -0
- package/docs/theming.md +53 -0
- package/package.json +142 -0
- package/scripts/generate-docs-index.ts +111 -0
- package/scripts/github-pull.ts +124 -0
- package/src/adapters/config/index.ts +1 -0
- package/src/adapters/config/node-config-loader.ts +36 -0
- package/src/adapters/fs/fs-docs-loader.ts +31 -0
- package/src/adapters/fs/index.ts +12 -0
- package/src/adapters/fs/memory-fs.ts +60 -0
- package/src/adapters/fs/node-fs.ts +116 -0
- package/src/adapters/git/index.ts +164 -0
- package/src/adapters/github/github-pull.ts +340 -0
- package/src/adapters/index.ts +6 -0
- package/src/adapters/velite.ts +112 -0
- package/src/adapters/vite/docs-watcher.ts +36 -0
- package/src/adapters/vite/index.ts +2 -0
- package/src/adapters/vite/polling-watcher.ts +89 -0
- package/src/code-block-enhancements/index.ts +3 -0
- package/src/config/index.ts +15 -0
- package/src/content/index.ts +11 -0
- package/src/content-validation/index.ts +5 -0
- package/src/index.ts +174 -0
- package/src/live-editor/index.ts +7 -0
- package/src/modules/analytics/application/plausible-adapter.ts +103 -0
- package/src/modules/analytics/application/umami-adapter.ts +109 -0
- package/src/modules/analytics/domain/events/index.ts +37 -0
- package/src/modules/analytics/domain/models/index.ts +35 -0
- package/src/modules/analytics/domain/operations/analytics-operations.ts +67 -0
- package/src/modules/analytics/domain/validators/index.ts +50 -0
- package/src/modules/analytics/index.ts +9 -0
- package/src/modules/analytics/ports/analytics-port.ts +17 -0
- package/src/modules/analytics/types/analytics.ts +16 -0
- package/src/modules/api-docs/application/type-docs-adapter.ts +34 -0
- package/src/modules/api-docs/domain/events/index.ts +42 -0
- package/src/modules/api-docs/domain/models/index.ts +40 -0
- package/src/modules/api-docs/domain/operations/api-docs-operations.ts +59 -0
- package/src/modules/api-docs/domain/validators/index.ts +56 -0
- package/src/modules/api-docs/index.ts +8 -0
- package/src/modules/api-docs/ports/api-docs-port.ts +21 -0
- package/src/modules/api-docs/types/api-docs.ts +15 -0
- package/src/modules/api-reference/openapi.ts +166 -0
- package/src/modules/api-reference/orpc.ts +94 -0
- package/src/modules/code-block-enhancements/application/usecases/index.ts +39 -0
- package/src/modules/code-block-enhancements/domain/code-operations.ts +72 -0
- package/src/modules/code-block-enhancements/domain/events/index.ts +58 -0
- package/src/modules/code-block-enhancements/domain/index.ts +11 -0
- package/src/modules/code-block-enhancements/domain/models/index.ts +36 -0
- package/src/modules/code-block-enhancements/domain/validators/index.ts +50 -0
- package/src/modules/code-block-enhancements/index.ts +10 -0
- package/src/modules/code-block-enhancements/ports/index.ts +18 -0
- package/src/modules/code-block-enhancements/types/index.ts +37 -0
- package/src/modules/code-execution/application/sandbox-adapter.ts +44 -0
- package/src/modules/code-execution/domain/events/index.ts +58 -0
- package/src/modules/code-execution/domain/models/index.ts +33 -0
- package/src/modules/code-execution/domain/operations/execution-operations.ts +60 -0
- package/src/modules/code-execution/domain/validators/index.ts +50 -0
- package/src/modules/code-execution/index.ts +8 -0
- package/src/modules/code-execution/ports/execution-port.ts +15 -0
- package/src/modules/code-execution/types/execution.ts +15 -0
- package/src/modules/comments/application/giscus-adapter.ts +55 -0
- package/src/modules/comments/domain/events/index.ts +55 -0
- package/src/modules/comments/domain/models/index.ts +30 -0
- package/src/modules/comments/domain/operations/comment-operations.ts +46 -0
- package/src/modules/comments/domain/validators/index.ts +50 -0
- package/src/modules/comments/index.ts +8 -0
- package/src/modules/comments/ports/comment-port.ts +18 -0
- package/src/modules/comments/types/comments.ts +19 -0
- package/src/modules/config/application/usecases/index.ts +1 -0
- package/src/modules/config/application/usecases/loadConfig.ts +38 -0
- package/src/modules/config/application/workflows/index.ts +1 -0
- package/src/modules/config/application/workflows/validateConfig.ts +14 -0
- package/src/modules/config/domain/events/index.ts +50 -0
- package/src/modules/config/domain/models/index.ts +9 -0
- package/src/modules/config/domain/operations/defaults.ts +74 -0
- package/src/modules/config/domain/operations/index.ts +2 -0
- package/src/modules/config/domain/operations/resolve.ts +40 -0
- package/src/modules/config/domain/validators/index.ts +152 -0
- package/src/modules/config/index.ts +23 -0
- package/src/modules/config/ports/index.ts +15 -0
- package/src/modules/config/types/index.ts +113 -0
- package/src/modules/config/types/public.ts +19 -0
- package/src/modules/content/application/cache/index.ts +6 -0
- package/src/modules/content/application/cache/parse-cache.ts +6 -0
- package/src/modules/content/application/cache/parseCache.ts +56 -0
- package/src/modules/content/application/usecases/index.ts +2 -0
- package/src/modules/content/application/usecases/load-doc-page.ts +1 -0
- package/src/modules/content/application/usecases/loadDocPage.ts +111 -0
- package/src/modules/content/application/usecases/scanDocsDir.ts +105 -0
- package/src/modules/content/application/workflows/index.ts +1 -0
- package/src/modules/content/application/workflows/processDocs.ts +51 -0
- package/src/modules/content/domain/events/index.ts +61 -0
- package/src/modules/content/domain/models/index.ts +13 -0
- package/src/modules/content/domain/operations/build-page.ts +146 -0
- package/src/modules/content/domain/operations/buildPage.ts +93 -0
- package/src/modules/content/domain/operations/index.ts +17 -0
- package/src/modules/content/domain/operations/parse.ts +241 -0
- package/src/modules/content/domain/validators/frontmatter.ts +82 -0
- package/src/modules/content/domain/validators/index.ts +6 -0
- package/src/modules/content/index.ts +27 -0
- package/src/modules/content/ports/index.ts +45 -0
- package/src/modules/content/ports/velite.ts +134 -0
- package/src/modules/content/types/index.ts +76 -0
- package/src/modules/content/types/public.ts +1 -0
- package/src/modules/content-linting/application/builtin-linting-adapter.ts +82 -0
- package/src/modules/content-linting/domain/events/index.ts +58 -0
- package/src/modules/content-linting/domain/models/index.ts +46 -0
- package/src/modules/content-linting/domain/operations/linting-operations.ts +82 -0
- package/src/modules/content-linting/domain/validators/index.ts +50 -0
- package/src/modules/content-linting/index.ts +8 -0
- package/src/modules/content-linting/ports/linting-port.ts +21 -0
- package/src/modules/content-linting/types/linting.ts +18 -0
- package/src/modules/content-validation/application/usecases/index.ts +18 -0
- package/src/modules/content-validation/domain/events/index.ts +7 -0
- package/src/modules/content-validation/domain/index.ts +16 -0
- package/src/modules/content-validation/domain/models/index.ts +33 -0
- package/src/modules/content-validation/domain/operations/index.ts +157 -0
- package/src/modules/content-validation/domain/types/index.ts +17 -0
- package/src/modules/content-validation/domain/validators/index.ts +8 -0
- package/src/modules/content-validation/index.ts +9 -0
- package/src/modules/content-validation/ports/index.ts +15 -0
- package/src/modules/content-validation/types/index.ts +5 -0
- package/src/modules/export/application/puppeteer-export-adapter.ts +63 -0
- package/src/modules/export/domain/events/index.ts +55 -0
- package/src/modules/export/domain/models/index.ts +38 -0
- package/src/modules/export/domain/operations/export-operations.ts +56 -0
- package/src/modules/export/domain/validators/index.ts +52 -0
- package/src/modules/export/index.ts +8 -0
- package/src/modules/export/ports/export-port.ts +15 -0
- package/src/modules/export/types/export.ts +15 -0
- package/src/modules/external-search/application/algolia-adapter.ts +182 -0
- package/src/modules/external-search/application/meilisearch-adapter.ts +161 -0
- package/src/modules/external-search/domain/events/index.ts +42 -0
- package/src/modules/external-search/domain/models/index.ts +42 -0
- package/src/modules/external-search/domain/operations/search-operations.ts +53 -0
- package/src/modules/external-search/domain/validators/index.ts +55 -0
- package/src/modules/external-search/index.ts +9 -0
- package/src/modules/external-search/ports/external-search-port.ts +23 -0
- package/src/modules/external-search/types/search.ts +19 -0
- package/src/modules/git-history/application/simple-git-adapter.ts +46 -0
- package/src/modules/git-history/domain/events/index.ts +34 -0
- package/src/modules/git-history/domain/models/index.ts +25 -0
- package/src/modules/git-history/domain/operations/git-history-operations.ts +56 -0
- package/src/modules/git-history/domain/validators/index.ts +52 -0
- package/src/modules/git-history/index.ts +8 -0
- package/src/modules/git-history/ports/git-history-port.ts +21 -0
- package/src/modules/git-history/types/git-history.ts +15 -0
- package/src/modules/i18n/application/fs-locale-loader.ts +39 -0
- package/src/modules/i18n/domain/events/index.ts +34 -0
- package/src/modules/i18n/domain/models/index.ts +34 -0
- package/src/modules/i18n/domain/operations/locale-operations.ts +74 -0
- package/src/modules/i18n/domain/validators/index.ts +52 -0
- package/src/modules/i18n/index.ts +8 -0
- package/src/modules/i18n/ports/locale-loader-port.ts +18 -0
- package/src/modules/i18n/types/locale.ts +17 -0
- package/src/modules/image-optimization/application/sharp-adapter.ts +58 -0
- package/src/modules/image-optimization/domain/events/index.ts +39 -0
- package/src/modules/image-optimization/domain/models/index.ts +27 -0
- package/src/modules/image-optimization/domain/operations/image-operations.ts +65 -0
- package/src/modules/image-optimization/domain/validators/index.ts +52 -0
- package/src/modules/image-optimization/index.ts +8 -0
- package/src/modules/image-optimization/ports/image-optimizer-port.ts +15 -0
- package/src/modules/image-optimization/types/image.ts +17 -0
- package/src/modules/live-editor/application/usecases/index.ts +60 -0
- package/src/modules/live-editor/domain/events/index.ts +37 -0
- package/src/modules/live-editor/domain/gitOperations.ts +110 -0
- package/src/modules/live-editor/domain/index.ts +15 -0
- package/src/modules/live-editor/domain/models/index.ts +21 -0
- package/src/modules/live-editor/domain/validators/index.ts +55 -0
- package/src/modules/live-editor/index.ts +14 -0
- package/src/modules/live-editor/ports/index.ts +49 -0
- package/src/modules/live-editor/types/index.ts +80 -0
- package/src/modules/live-preview/application/vite-preview-adapter.ts +20 -0
- package/src/modules/live-preview/domain/operations/preview-operations.ts +56 -0
- package/src/modules/live-preview/index.ts +8 -0
- package/src/modules/live-preview/ports/preview-port.ts +20 -0
- package/src/modules/live-preview/types/preview.ts +16 -0
- package/src/modules/mdx/application/mdx-js-adapter.ts +38 -0
- package/src/modules/mdx/domain/operations/parse-mdx.ts +43 -0
- package/src/modules/mdx/index.ts +8 -0
- package/src/modules/mdx/ports/mdx-parser-port.ts +15 -0
- package/src/modules/mdx/types/mdx.ts +14 -0
- package/src/modules/navigation/application/usecases/buildNav.ts +15 -0
- package/src/modules/navigation/application/usecases/buildSidebar.ts +45 -0
- package/src/modules/navigation/application/usecases/index.ts +2 -0
- package/src/modules/navigation/domain/events/index.ts +30 -0
- package/src/modules/navigation/domain/models/index.ts +7 -0
- package/src/modules/navigation/domain/operations/group.ts +79 -0
- package/src/modules/navigation/domain/operations/index.ts +10 -0
- package/src/modules/navigation/domain/operations/sort.ts +27 -0
- package/src/modules/navigation/domain/shared/utils/array.ts +39 -0
- package/src/modules/navigation/domain/validators/index.ts +88 -0
- package/src/modules/navigation/index.ts +15 -0
- package/src/modules/navigation/ports/index.ts +9 -0
- package/src/modules/navigation/types/index.ts +38 -0
- package/src/modules/navigation/types/public.ts +9 -0
- package/src/modules/openapi/application/swagger-parser-adapter.ts +178 -0
- package/src/modules/openapi/domain/operations/parse-openapi.ts +46 -0
- package/src/modules/openapi/index.ts +8 -0
- package/src/modules/openapi/ports/openapi-parser-port.ts +15 -0
- package/src/modules/openapi/types/openapi.ts +37 -0
- package/src/modules/plugin/application/usecases/build-virtual-modules.ts +53 -0
- package/src/modules/plugin/application/usecases/index.ts +1 -0
- package/src/modules/plugin/application/workflows/build-docs.ts +79 -0
- package/src/modules/plugin/application/workflows/index.ts +1 -0
- package/src/modules/plugin/domain/events/index.ts +62 -0
- package/src/modules/plugin/domain/models/index.ts +1 -0
- package/src/modules/plugin/domain/operations/build-virtual.ts +76 -0
- package/src/modules/plugin/domain/operations/index.ts +11 -0
- package/src/modules/plugin/domain/validators/index.ts +30 -0
- package/src/modules/plugin/index.ts +14 -0
- package/src/modules/plugin/ports/index.ts +21 -0
- package/src/modules/plugin/types/index.ts +27 -0
- package/src/modules/plugin/types/public.ts +1 -0
- package/src/modules/search/application/usecases/index.ts +1 -0
- package/src/modules/search/application/usecases/indexPages.ts +15 -0
- package/src/modules/search/domain/events/index.ts +32 -0
- package/src/modules/search/domain/models/index.ts +1 -0
- package/src/modules/search/domain/operations/build-index.ts +6 -0
- package/src/modules/search/domain/operations/buildIndex.ts +70 -0
- package/src/modules/search/domain/operations/index.ts +7 -0
- package/src/modules/search/domain/operations/query.ts +186 -0
- package/src/modules/search/domain/validators/index.ts +32 -0
- package/src/modules/search/index.ts +9 -0
- package/src/modules/search/types/index.ts +22 -0
- package/src/modules/search/types/public.ts +1 -0
- package/src/modules/search-analytics/application/local-storage-adapter.ts +68 -0
- package/src/modules/search-analytics/domain/operations/search-analytics-operations.ts +74 -0
- package/src/modules/search-analytics/index.ts +8 -0
- package/src/modules/search-analytics/ports/search-analytics-port.ts +20 -0
- package/src/modules/search-analytics/types/search-analytics.ts +16 -0
- package/src/modules/search-highlighting/application/dom-highlighting-adapter.ts +36 -0
- package/src/modules/search-highlighting/domain/operations/highlighting-operations.ts +72 -0
- package/src/modules/search-highlighting/index.ts +8 -0
- package/src/modules/search-highlighting/ports/highlighting-port.ts +14 -0
- package/src/modules/search-highlighting/types/highlighting.ts +15 -0
- package/src/modules/seo-optimization/application/usecases/index.ts +18 -0
- package/src/modules/seo-optimization/domain/index.ts +12 -0
- package/src/modules/seo-optimization/domain/seoOperations.ts +92 -0
- package/src/modules/seo-optimization/index.ts +8 -0
- package/src/modules/seo-optimization/ports/index.ts +18 -0
- package/src/modules/seo-optimization/types/index.ts +41 -0
- package/src/modules/theme/application/css-theme-adapter.ts +50 -0
- package/src/modules/theme/domain/operations/theme-operations.ts +68 -0
- package/src/modules/theme/index.ts +8 -0
- package/src/modules/theme/ports/theme-port.ts +8 -0
- package/src/modules/theme/types/theme.ts +12 -0
- package/src/modules/toc/application/markdown-toc-adapter.ts +106 -0
- package/src/modules/toc/domain/operations/toc-operations.ts +92 -0
- package/src/modules/toc/index.ts +8 -0
- package/src/modules/toc/ports/toc-port.ts +21 -0
- package/src/modules/toc/types/toc.ts +17 -0
- package/src/modules/toc-generator/application/usecases/index.ts +30 -0
- package/src/modules/toc-generator/domain/index.ts +12 -0
- package/src/modules/toc-generator/domain/tocOperations.ts +104 -0
- package/src/modules/toc-generator/index.ts +9 -0
- package/src/modules/toc-generator/ports/index.ts +20 -0
- package/src/modules/toc-generator/types/index.ts +50 -0
- package/src/modules/version-control/domain/events/index.ts +39 -0
- package/src/modules/version-control/domain/models/index.ts +25 -0
- package/src/modules/version-control/domain/validators/index.ts +50 -0
- package/src/modules/versioning/application/fs-version-loader.ts +44 -0
- package/src/modules/versioning/domain/operations/versioning-operations.ts +80 -0
- package/src/modules/versioning/index.ts +8 -0
- package/src/modules/versioning/ports/version-loader-port.ts +18 -0
- package/src/modules/versioning/types/versioning.ts +17 -0
- package/src/modules/workflow/domain/events/index.ts +55 -0
- package/src/modules/workflow/domain/models/index.ts +40 -0
- package/src/modules/workflow/domain/validators/index.ts +50 -0
- package/src/modules/yaml-parser/application/js-yaml-adapter.ts +34 -0
- package/src/modules/yaml-parser/domain/operations/parse-yaml.ts +58 -0
- package/src/modules/yaml-parser/index.ts +7 -0
- package/src/modules/yaml-parser/ports/yaml-parser-port.ts +18 -0
- package/src/navigation/index.ts +9 -0
- package/src/plugin/index.ts +4 -0
- package/src/presentation/index.ts +1 -0
- package/src/presentation/live-editor/index.ts +22 -0
- package/src/presentation/live-editor/live-editor-component.ts +219 -0
- package/src/presentation/live-editor/virtual-module.ts +16 -0
- package/src/presentation/solid/CodeBlock.tsx +120 -0
- package/src/presentation/solid/Toc.tsx +58 -0
- package/src/presentation/solid/index.ts +11 -0
- package/src/presentation/vite-plugin/docs-plugin.ts +212 -0
- package/src/presentation/vite-plugin/index.ts +9 -0
- package/src/presentation/vite-plugin/transform-markdown.ts +26 -0
- package/src/runtime/analytics.ts +59 -0
- package/src/runtime/api-diff.ts +115 -0
- package/src/runtime/app.tsx +32 -0
- package/src/runtime/components/AccentPicker.tsx +114 -0
- package/src/runtime/components/ApiPlayground.tsx +332 -0
- package/src/runtime/components/AskAiDialog.tsx +127 -0
- package/src/runtime/components/BackToTop.tsx +38 -0
- package/src/runtime/components/Banner.tsx +75 -0
- package/src/runtime/components/Breadcrumbs.tsx +42 -0
- package/src/runtime/components/CollectionDropdown.tsx +85 -0
- package/src/runtime/components/CustomizeDrawer.tsx +195 -0
- package/src/runtime/components/DocMarkdown.tsx +579 -0
- package/src/runtime/components/DocPrevNext.tsx +54 -0
- package/src/runtime/components/DocToc.tsx +147 -0
- package/src/runtime/components/DocsDropdown.tsx +184 -0
- package/src/runtime/components/Footer.tsx +52 -0
- package/src/runtime/components/GitHubAuth.tsx +82 -0
- package/src/runtime/components/GitHubStats.tsx +73 -0
- package/src/runtime/components/Head.tsx +225 -0
- package/src/runtime/components/LocaleDropdown.tsx +75 -0
- package/src/runtime/components/MobileBottomNav.tsx +111 -0
- package/src/runtime/components/NotFound.tsx +139 -0
- package/src/runtime/components/PageActions.tsx +144 -0
- package/src/runtime/components/RelatedDocs.tsx +50 -0
- package/src/runtime/components/ScrollProgress.tsx +32 -0
- package/src/runtime/components/SearchPalette.tsx +333 -0
- package/src/runtime/components/ShowcaseCard.tsx +87 -0
- package/src/runtime/components/SidebarNav.tsx +237 -0
- package/src/runtime/components/Skeleton.tsx +39 -0
- package/src/runtime/components/ThemeToggle.tsx +26 -0
- package/src/runtime/components/TopNav.tsx +270 -0
- package/src/runtime/components/VersionDropdown.tsx +189 -0
- package/src/runtime/components/markdown/components.ts +280 -0
- package/src/runtime/config.ts +225 -0
- package/src/runtime/content.ts +451 -0
- package/src/runtime/context.tsx +30 -0
- package/src/runtime/data.ts +93 -0
- package/src/runtime/github.ts +226 -0
- package/src/runtime/icons.ts +32 -0
- package/src/runtime/index.ts +153 -0
- package/src/runtime/layouts/DocsLayout.tsx +144 -0
- package/src/runtime/markdown-content.css +996 -0
- package/src/runtime/pages/AbTestPage.tsx +88 -0
- package/src/runtime/pages/AnalyticsPage.tsx +90 -0
- package/src/runtime/pages/ApiDiffPage.tsx +86 -0
- package/src/runtime/pages/ApiEndpointPage.tsx +248 -0
- package/src/runtime/pages/BuilderPage.tsx +150 -0
- package/src/runtime/pages/ChangelogPage.tsx +210 -0
- package/src/runtime/pages/CollectionPage.tsx +140 -0
- package/src/runtime/pages/CommunityPage.tsx +384 -0
- package/src/runtime/pages/CreatePage.tsx +448 -0
- package/src/runtime/pages/DocPage.tsx +193 -0
- package/src/runtime/pages/EditPage.tsx +201 -0
- package/src/runtime/pages/HomePage.tsx +187 -0
- package/src/runtime/pages/IssuesPage.tsx +140 -0
- package/src/runtime/pages/OAuthCallbackPage.tsx +80 -0
- package/src/runtime/pages/PluginsPage.tsx +249 -0
- package/src/runtime/pages/ShowcasePage.tsx +202 -0
- package/src/runtime/pwa.ts +39 -0
- package/src/runtime/router.tsx +192 -0
- package/src/runtime/seo.ts +192 -0
- package/src/runtime/theme.css +175 -0
- package/src/runtime/theme.ts +224 -0
- package/src/runtime/types.ts +217 -0
- package/src/search/index.ts +6 -0
- package/src/seo-optimization/index.ts +4 -0
- package/src/shared/constants/index.ts +26 -0
- package/src/shared/errors/index.ts +195 -0
- package/src/shared/index.ts +27 -0
- package/src/shared/types/brand.ts +14 -0
- package/src/shared/types/client.d.ts +45 -0
- package/src/shared/types/either.ts +38 -0
- package/src/shared/types/index.ts +42 -0
- package/src/shared/types/option.ts +52 -0
- package/src/shared/types/result.ts +81 -0
- package/src/shared/utils/array.ts +97 -0
- package/src/shared/utils/index.ts +46 -0
- package/src/shared/utils/object.ts +47 -0
- package/src/shared/utils/string.ts +74 -0
- package/src/toc-generator/index.ts +1 -0
- package/src/types/js-yaml.d.ts +42 -0
- package/src/worker.ts +462 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./application/usecases";
|
|
2
|
+
export * from "./application/workflows";
|
|
3
|
+
export {
|
|
4
|
+
pluginConfigResolved,
|
|
5
|
+
pluginError,
|
|
6
|
+
pluginFileWatched,
|
|
7
|
+
pluginInitialized,
|
|
8
|
+
pluginReloaded,
|
|
9
|
+
pluginVirtualModuleEmitted,
|
|
10
|
+
} from "./domain/events";
|
|
11
|
+
export * from "./domain/operations";
|
|
12
|
+
export { validateResolvedConfig } from "./domain/validators";
|
|
13
|
+
export type { DocsLoader, DocsWatcher } from "./ports";
|
|
14
|
+
export * from "./types/public";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin module ports — interfaces for I/O the plugin needs from adapters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ResolvedDocsConfig } from "@create-docs/modules/config";
|
|
6
|
+
import type { DocPage } from "@create-docs/modules/content";
|
|
7
|
+
import type { AppError } from "@create-docs/shared/errors";
|
|
8
|
+
import type { Result } from "@create-docs/shared/types/result";
|
|
9
|
+
|
|
10
|
+
export interface DocsLoader {
|
|
11
|
+
loadAll(
|
|
12
|
+
config: ResolvedDocsConfig,
|
|
13
|
+
): Promise<Result<readonly DocPage[], AppError>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DocsWatcher {
|
|
17
|
+
watch(
|
|
18
|
+
docsDir: string,
|
|
19
|
+
onChange: (filePath: string) => void,
|
|
20
|
+
): { close: () => void };
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Brand } from "@create-docs/shared/types";
|
|
2
|
+
|
|
3
|
+
export type VirtualModuleId = Brand<string, "VirtualModuleId">;
|
|
4
|
+
export const VirtualModuleId = (s: string): VirtualModuleId =>
|
|
5
|
+
s as VirtualModuleId;
|
|
6
|
+
|
|
7
|
+
export const VIRTUAL_DATA: VirtualModuleId =
|
|
8
|
+
VirtualModuleId("virtual:docs/data");
|
|
9
|
+
export const VIRTUAL_SEARCH_INDEX: VirtualModuleId = VirtualModuleId(
|
|
10
|
+
"virtual:docs/search-index",
|
|
11
|
+
);
|
|
12
|
+
export const VIRTUAL_SIDEBAR: VirtualModuleId = VirtualModuleId(
|
|
13
|
+
"virtual:docs/sidebar",
|
|
14
|
+
);
|
|
15
|
+
export const VIRTUAL_NAV: VirtualModuleId = VirtualModuleId("virtual:docs/nav");
|
|
16
|
+
|
|
17
|
+
export interface PluginContext {
|
|
18
|
+
readonly root: string;
|
|
19
|
+
readonly mode: "dev" | "build" | "test";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PluginHooks {
|
|
23
|
+
readonly configResolved?: (config: PluginContext) => void;
|
|
24
|
+
readonly serverClosed?: () => void;
|
|
25
|
+
readonly fileChanged?: (filePath: string) => void;
|
|
26
|
+
readonly docsReloaded?: () => void;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { PluginContext, PluginHooks, VirtualModuleId } from "./index";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { indexPages } from "./indexPages";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use case: index all doc pages into a searchable structure.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DocPage } from "@create-docs/modules/content";
|
|
6
|
+
import type { AppError } from "@create-docs/shared/errors";
|
|
7
|
+
import { ok, type Result } from "@create-docs/shared/types/result";
|
|
8
|
+
import { buildSearchIndex } from "../../domain/operations/buildIndex";
|
|
9
|
+
import type { SearchIndex } from "../../types";
|
|
10
|
+
|
|
11
|
+
export type IndexPages = (
|
|
12
|
+
pages: readonly DocPage[],
|
|
13
|
+
) => Result<SearchIndex, AppError>;
|
|
14
|
+
|
|
15
|
+
export const indexPages: IndexPages = (pages) => ok(buildSearchIndex(pages));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type SearchEvent =
|
|
2
|
+
| { readonly type: "search.index_built"; readonly entriesCount: number }
|
|
3
|
+
| {
|
|
4
|
+
readonly type: "search.index_rebuilt";
|
|
5
|
+
readonly reason: "content_changed" | "config_changed" | "manual";
|
|
6
|
+
}
|
|
7
|
+
| {
|
|
8
|
+
readonly type: "search.query_executed";
|
|
9
|
+
readonly term: string;
|
|
10
|
+
readonly hits: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const searchIndexBuilt = (entriesCount: number): SearchEvent => ({
|
|
14
|
+
type: "search.index_built",
|
|
15
|
+
entriesCount,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const searchIndexRebuilt = (
|
|
19
|
+
reason: "content_changed" | "config_changed" | "manual",
|
|
20
|
+
): SearchEvent => ({
|
|
21
|
+
type: "search.index_rebuilt",
|
|
22
|
+
reason,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const searchQueryExecuted = (
|
|
26
|
+
term: string,
|
|
27
|
+
hits: number,
|
|
28
|
+
): SearchEvent => ({
|
|
29
|
+
type: "search.query_executed",
|
|
30
|
+
term,
|
|
31
|
+
hits,
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { SearchEntry, SearchIndex } from "../../types";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure operations for building and querying a search index.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { DocPage } from "@create-docs/modules/content";
|
|
6
|
+
import { SEARCH_CONTENT_LIMIT } from "@create-docs/shared/constants";
|
|
7
|
+
import { truncate } from "@create-docs/shared/utils/string";
|
|
8
|
+
import type { SearchEntry, SearchIndex } from "../../types";
|
|
9
|
+
|
|
10
|
+
export const pageToSearchEntry = (page: DocPage): SearchEntry => {
|
|
11
|
+
const text = plainText(page.content);
|
|
12
|
+
const terms = tokenize(`${page.title} ${page.description ?? ""} ${text}`);
|
|
13
|
+
return {
|
|
14
|
+
id: String(page.id),
|
|
15
|
+
slug: String(page.slug),
|
|
16
|
+
title: page.title,
|
|
17
|
+
description: page.description ?? "",
|
|
18
|
+
content: truncate(text, SEARCH_CONTENT_LIMIT, ""),
|
|
19
|
+
terms,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const buildSearchIndex = (pages: readonly DocPage[]): SearchIndex => {
|
|
24
|
+
const entries = pages.map(pageToSearchEntry);
|
|
25
|
+
const inverted: Record<string, string[]> = {};
|
|
26
|
+
let totalLength = 0;
|
|
27
|
+
for (const e of entries) {
|
|
28
|
+
const seen = new Set<string>();
|
|
29
|
+
for (const term of e.terms) {
|
|
30
|
+
if (seen.has(term)) continue;
|
|
31
|
+
seen.add(term);
|
|
32
|
+
const list = inverted[term] ?? [];
|
|
33
|
+
list.push(e.id);
|
|
34
|
+
inverted[term] = list;
|
|
35
|
+
}
|
|
36
|
+
totalLength += e.terms.length;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
entries,
|
|
40
|
+
inverted,
|
|
41
|
+
avgDocLength: entries.length === 0 ? 0 : totalLength / entries.length,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Strip markdown formatting for full-text search. */
|
|
46
|
+
export const plainText = (md: string): string =>
|
|
47
|
+
md
|
|
48
|
+
.replace(/```[\s\S]*?```/g, " ") // fenced code
|
|
49
|
+
.replace(/~~~[\s\S]*?~~~/g, " ") // tilde code
|
|
50
|
+
.replace(/`[^`\n]*`/g, " ") // inline code
|
|
51
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1") // images
|
|
52
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") // links
|
|
53
|
+
.replace(/^#{1,6}\s+/gm, "") // headings
|
|
54
|
+
.replace(/^\s*[-*+]\s+/gm, "") // bullets
|
|
55
|
+
.replace(/^\s*\d+\.\s+/gm, "") // ordered list
|
|
56
|
+
.replace(/^>\s?/gm, "") // blockquote
|
|
57
|
+
.replace(/[*_~]+/g, "") // emphasis
|
|
58
|
+
.replace(/\s+/g, " ")
|
|
59
|
+
.trim();
|
|
60
|
+
|
|
61
|
+
/** Tokenize a string into lowercase terms (length >= 2), stripping diacritics. */
|
|
62
|
+
export const tokenize = (s: string): string[] => {
|
|
63
|
+
if (s.length === 0) return [];
|
|
64
|
+
return s
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.normalize("NFD")
|
|
67
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
68
|
+
.split(/[^\p{Letter}\p{Number}]+/u)
|
|
69
|
+
.filter((t) => t.length >= 2);
|
|
70
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure search query operations.
|
|
3
|
+
*
|
|
4
|
+
* Scoring = BM25-lite over tokenized terms + title prefix boost + Levenshtein
|
|
5
|
+
* fuzzy fallback (distance <= 2) when the literal term is not found.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SearchEntry, SearchIndex } from "../../types";
|
|
9
|
+
import { tokenize } from "./buildIndex";
|
|
10
|
+
|
|
11
|
+
export interface SearchQuery {
|
|
12
|
+
readonly term: string;
|
|
13
|
+
readonly limit?: number;
|
|
14
|
+
/** When true, allow fuzzy (Levenshtein <=2) fallback. Default: true. */
|
|
15
|
+
readonly fuzzy?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SearchHit {
|
|
19
|
+
readonly entry: SearchEntry;
|
|
20
|
+
readonly score: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BM25_K1 = 1.2;
|
|
24
|
+
const BM25_B = 0.75;
|
|
25
|
+
const TITLE_BOOST = 5;
|
|
26
|
+
const PREFIX_BOOST = 2;
|
|
27
|
+
const FUZZY_PENALTY = 0.5;
|
|
28
|
+
export const levenshtein = (a: string, b: string, max = 3): number => {
|
|
29
|
+
if (a === b) return 0;
|
|
30
|
+
const la = a.length;
|
|
31
|
+
const lb = b.length;
|
|
32
|
+
if (Math.abs(la - lb) > max) return max + 1;
|
|
33
|
+
if (la === 0) return lb;
|
|
34
|
+
if (lb === 0) return la;
|
|
35
|
+
const prev: number[] = new Array<number>(lb + 1);
|
|
36
|
+
const curr: number[] = new Array<number>(lb + 1);
|
|
37
|
+
for (let j = 0; j <= lb; j++) prev[j] = j;
|
|
38
|
+
for (let i = 1; i <= la; i++) {
|
|
39
|
+
curr[0] = i;
|
|
40
|
+
let rowMin = curr[0] ?? i;
|
|
41
|
+
for (let j = 1; j <= lb; j++) {
|
|
42
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
43
|
+
const c1 = (curr[j - 1] ?? 0) + 1;
|
|
44
|
+
const c2 = (prev[j] ?? 0) + 1;
|
|
45
|
+
const c3 = (prev[j - 1] ?? 0) + cost;
|
|
46
|
+
curr[j] = Math.min(c1, c2, c3);
|
|
47
|
+
const v = curr[j];
|
|
48
|
+
if (v !== undefined && v < rowMin) rowMin = v;
|
|
49
|
+
}
|
|
50
|
+
if (rowMin > max) return max + 1;
|
|
51
|
+
for (let j = 0; j <= lb; j++) {
|
|
52
|
+
const v = curr[j];
|
|
53
|
+
if (v !== undefined) prev[j] = v;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return prev[lb] ?? 0;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const bm25 = (
|
|
60
|
+
tf: number,
|
|
61
|
+
df: number,
|
|
62
|
+
docLength: number,
|
|
63
|
+
avgDocLength: number,
|
|
64
|
+
totalDocs: number,
|
|
65
|
+
): number => {
|
|
66
|
+
if (tf === 0) return 0;
|
|
67
|
+
const idf = Math.log(1 + (totalDocs - df + 0.5) / (df + 0.5));
|
|
68
|
+
const norm = 1 - BM25_B + (BM25_B * docLength) / Math.max(1, avgDocLength);
|
|
69
|
+
return idf * ((tf * (BM25_K1 + 1)) / (tf + BM25_K1 * norm));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Case-insensitive, tokenized search with BM25-lite scoring. */
|
|
73
|
+
export const search = (
|
|
74
|
+
index: SearchIndex,
|
|
75
|
+
query: SearchQuery,
|
|
76
|
+
): readonly SearchHit[] => {
|
|
77
|
+
const rawTerm = query.term.trim();
|
|
78
|
+
if (rawTerm.length === 0) return [];
|
|
79
|
+
const fuzzy = query.fuzzy !== false;
|
|
80
|
+
const limit = query.limit ?? 20;
|
|
81
|
+
|
|
82
|
+
const terms = tokenize(rawTerm);
|
|
83
|
+
if (terms.length === 0) return [];
|
|
84
|
+
|
|
85
|
+
const totalDocs = index.entries.length;
|
|
86
|
+
const avgDoc = index.avgDocLength;
|
|
87
|
+
const idToEntry = new Map<string, SearchEntry>();
|
|
88
|
+
for (const e of index.entries) idToEntry.set(e.id, e);
|
|
89
|
+
|
|
90
|
+
interface ScoredDoc {
|
|
91
|
+
score: number;
|
|
92
|
+
matchedExact: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const docScores = new Map<string, ScoredDoc>();
|
|
96
|
+
|
|
97
|
+
for (const term of terms) {
|
|
98
|
+
// 1. Exact term lookup
|
|
99
|
+
const exact = index.inverted[term];
|
|
100
|
+
if (exact) {
|
|
101
|
+
for (const id of exact) {
|
|
102
|
+
const entry = idToEntry.get(id);
|
|
103
|
+
if (!entry) continue;
|
|
104
|
+
let tf = 0;
|
|
105
|
+
for (const t of entry.terms) if (t === term) tf++;
|
|
106
|
+
const score = bm25(
|
|
107
|
+
tf,
|
|
108
|
+
exact.length,
|
|
109
|
+
entry.terms.length,
|
|
110
|
+
avgDoc,
|
|
111
|
+
totalDocs,
|
|
112
|
+
);
|
|
113
|
+
const prev = docScores.get(id);
|
|
114
|
+
docScores.set(id, {
|
|
115
|
+
score: (prev?.score ?? 0) + score,
|
|
116
|
+
matchedExact: prev?.matchedExact ?? true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 2. Fuzzy fallback: find the best matching key per candidate doc.
|
|
123
|
+
if (!fuzzy) continue;
|
|
124
|
+
const seen = new Set<string>();
|
|
125
|
+
const fuzzyHits: { id: string; matchedKey: string; dist: number }[] = [];
|
|
126
|
+
for (const key of Object.keys(index.inverted)) {
|
|
127
|
+
if (Math.abs(key.length - term.length) > 2) continue;
|
|
128
|
+
const d = levenshtein(key, term, 2);
|
|
129
|
+
if (d <= 2) {
|
|
130
|
+
const list = index.inverted[key] ?? [];
|
|
131
|
+
for (const id of list) {
|
|
132
|
+
if (seen.has(id)) continue;
|
|
133
|
+
seen.add(id);
|
|
134
|
+
fuzzyHits.push({ id, matchedKey: key, dist: d });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. Per candidate, find the closest matching term in the entry's own terms
|
|
140
|
+
// so we can score via bm25 with a real tf.
|
|
141
|
+
for (const hit of fuzzyHits) {
|
|
142
|
+
const entry = idToEntry.get(hit.id);
|
|
143
|
+
if (!entry) continue;
|
|
144
|
+
// Reuse dist from index key (already a good proxy). Compute tf for that key.
|
|
145
|
+
let tf = 0;
|
|
146
|
+
for (const t of entry.terms) if (t === hit.matchedKey) tf++;
|
|
147
|
+
if (tf === 0) {
|
|
148
|
+
// Fallback: count how many entry terms are within distance 2 of the query term.
|
|
149
|
+
for (const t of entry.terms) {
|
|
150
|
+
if (Math.abs(t.length - term.length) > 2) continue;
|
|
151
|
+
if (levenshtein(t, term, 2) <= 2) tf++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const score =
|
|
155
|
+
bm25(tf, fuzzyHits.length, entry.terms.length, avgDoc, totalDocs) *
|
|
156
|
+
FUZZY_PENALTY;
|
|
157
|
+
const prev = docScores.get(hit.id);
|
|
158
|
+
docScores.set(hit.id, {
|
|
159
|
+
score: (prev?.score ?? 0) + score,
|
|
160
|
+
matchedExact: prev?.matchedExact ?? false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Title boost: substring or prefix match on the title gets extra weight.
|
|
166
|
+
for (const e of index.entries) {
|
|
167
|
+
const cur = docScores.get(e.id);
|
|
168
|
+
if (!cur || cur.score === 0) continue;
|
|
169
|
+
const titleLow = e.title.toLowerCase();
|
|
170
|
+
if (titleLow.includes(rawTerm.toLowerCase())) {
|
|
171
|
+
cur.score += TITLE_BOOST;
|
|
172
|
+
}
|
|
173
|
+
if (titleLow.startsWith(terms[0] ?? "")) {
|
|
174
|
+
cur.score += PREFIX_BOOST;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const hits: SearchHit[] = [];
|
|
179
|
+
for (const [id, cur] of docScores.entries()) {
|
|
180
|
+
if (cur.score <= 0) continue;
|
|
181
|
+
const entry = idToEntry.get(id);
|
|
182
|
+
if (entry) hits.push({ entry, score: cur.score });
|
|
183
|
+
}
|
|
184
|
+
hits.sort((a, b) => b.score - a.score);
|
|
185
|
+
return hits.slice(0, limit);
|
|
186
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Search Domain Schemas - Arktype validation schemas
|
|
2
|
+
// Following /follow-arktype workflow for type-safe runtime validation
|
|
3
|
+
|
|
4
|
+
import { validationError } from "@create-docs/shared/errors";
|
|
5
|
+
import { err, ok, type Result } from "@create-docs/shared/types/result";
|
|
6
|
+
import { type Type, type } from "arktype";
|
|
7
|
+
import type { SearchEntry } from "../../types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Search Entry Schema
|
|
11
|
+
*/
|
|
12
|
+
export const searchEntrySchema: Type = type({
|
|
13
|
+
id: "string >= 1",
|
|
14
|
+
slug: "string >= 1",
|
|
15
|
+
title: "string >= 1",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type SearchEntrySchema = typeof searchEntrySchema.infer;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate search entry using Arktype
|
|
22
|
+
*/
|
|
23
|
+
export const validateSearchEntry = (
|
|
24
|
+
e: SearchEntry,
|
|
25
|
+
path: string,
|
|
26
|
+
): Result<SearchEntry, ReturnType<typeof validationError>> => {
|
|
27
|
+
const result = searchEntrySchema(e);
|
|
28
|
+
if (result instanceof type.errors) {
|
|
29
|
+
return err(validationError(result.summary, { path }));
|
|
30
|
+
}
|
|
31
|
+
return ok(e);
|
|
32
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./application/usecases";
|
|
2
|
+
export {
|
|
3
|
+
searchIndexBuilt,
|
|
4
|
+
searchIndexRebuilt,
|
|
5
|
+
searchQueryExecuted,
|
|
6
|
+
} from "./domain/events";
|
|
7
|
+
export * from "./domain/operations";
|
|
8
|
+
export { validateSearchEntry } from "./domain/validators";
|
|
9
|
+
export * from "./types/public";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Brand } from "@create-docs/shared/types";
|
|
2
|
+
|
|
3
|
+
export type SearchDocId = Brand<string, "SearchDocId">;
|
|
4
|
+
export const SearchDocId = (s: string): SearchDocId => s as SearchDocId;
|
|
5
|
+
|
|
6
|
+
export interface SearchEntry {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly slug: string;
|
|
9
|
+
readonly title: string;
|
|
10
|
+
readonly description: string;
|
|
11
|
+
readonly content: string;
|
|
12
|
+
/** Pre-tokenized content terms (lowercase, length >= 2). */
|
|
13
|
+
readonly terms: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SearchIndex {
|
|
17
|
+
readonly entries: readonly SearchEntry[];
|
|
18
|
+
/** Token → entry ids that contain it (BM25-ready). */
|
|
19
|
+
readonly inverted: Readonly<Record<string, readonly string[]>>;
|
|
20
|
+
/** Average document length across the corpus (used by BM25). */
|
|
21
|
+
readonly avgDocLength: number;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { SearchDocId, SearchEntry, SearchIndex } from "./index";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local storage adapter implementation.
|
|
3
|
+
* Provides search analytics using browser local storage.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SearchAnalyticsPort } from "../ports/search-analytics-port";
|
|
7
|
+
import type { SearchEvent } from "../types/search-analytics";
|
|
8
|
+
|
|
9
|
+
const STORAGE_KEY = "search-analytics-events";
|
|
10
|
+
const MAX_EVENTS = 1000;
|
|
11
|
+
|
|
12
|
+
export const createLocalStorageAdapter = (): SearchAnalyticsPort => ({
|
|
13
|
+
trackEvent: async (event: SearchEvent, _config?: unknown) => {
|
|
14
|
+
if (typeof window === "undefined") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
20
|
+
const events: SearchEvent[] = stored ? JSON.parse(stored) : [];
|
|
21
|
+
|
|
22
|
+
events.push(event);
|
|
23
|
+
|
|
24
|
+
if (events.length > MAX_EVENTS) {
|
|
25
|
+
events.shift();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(events));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("Failed to track search event:", error);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
getAnalytics: async (dateRange?: unknown, _config?: unknown) => {
|
|
35
|
+
if (typeof window === "undefined") {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
41
|
+
if (!stored) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const events: SearchEvent[] = JSON.parse(stored);
|
|
46
|
+
|
|
47
|
+
if (dateRange && typeof dateRange === "object") {
|
|
48
|
+
const { since, until } = dateRange as {
|
|
49
|
+
since?: string;
|
|
50
|
+
until?: string;
|
|
51
|
+
};
|
|
52
|
+
if (since || until) {
|
|
53
|
+
return events.filter((event) => {
|
|
54
|
+
const eventDate = new Date(event.timestamp);
|
|
55
|
+
if (since && eventDate < new Date(since)) return false;
|
|
56
|
+
if (until && eventDate > new Date(until)) return false;
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return events;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error("Failed to get search analytics:", error);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure search analytics operations.
|
|
3
|
+
* No I/O. No exceptions thrown. Returns Result types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Result } from "@create-docs/shared/types";
|
|
7
|
+
import { err, ok } from "@create-docs/shared/types";
|
|
8
|
+
import type {
|
|
9
|
+
SearchAnalyticsConfig,
|
|
10
|
+
SearchEvent,
|
|
11
|
+
} from "../../types/search-analytics";
|
|
12
|
+
|
|
13
|
+
export type SearchAnalyticsError = {
|
|
14
|
+
readonly kind: "search-analytics-error";
|
|
15
|
+
readonly message: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Aggregate search queries by frequency.
|
|
20
|
+
*/
|
|
21
|
+
export const aggregateQueries = (
|
|
22
|
+
events: readonly SearchEvent[],
|
|
23
|
+
): Result<Record<string, number>, SearchAnalyticsError> => {
|
|
24
|
+
try {
|
|
25
|
+
const counts: Record<string, number> = {};
|
|
26
|
+
|
|
27
|
+
for (const event of events) {
|
|
28
|
+
if (event.type === "query") {
|
|
29
|
+
const query = event.query.toLowerCase();
|
|
30
|
+
counts[query] = (counts[query] || 0) + 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return ok(counts);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return err({
|
|
37
|
+
kind: "search-analytics-error",
|
|
38
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculate search success rate.
|
|
45
|
+
*/
|
|
46
|
+
export const calculateSuccessRate = (
|
|
47
|
+
events: readonly SearchEvent[],
|
|
48
|
+
): Result<number, SearchAnalyticsError> => {
|
|
49
|
+
try {
|
|
50
|
+
const queryEvents = events.filter((e) => e.type === "query");
|
|
51
|
+
const noResultEvents = events.filter((e) => e.type === "no-results");
|
|
52
|
+
|
|
53
|
+
if (queryEvents.length === 0) return ok(100);
|
|
54
|
+
|
|
55
|
+
const successRate =
|
|
56
|
+
((queryEvents.length - noResultEvents.length) / queryEvents.length) * 100;
|
|
57
|
+
return ok(successRate);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return err({
|
|
60
|
+
kind: "search-analytics-error",
|
|
61
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate search analytics config.
|
|
68
|
+
*/
|
|
69
|
+
export const validateSearchAnalyticsConfig = (
|
|
70
|
+
config: SearchAnalyticsConfig,
|
|
71
|
+
): boolean => {
|
|
72
|
+
if (!config.enabled) return false;
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for the search-analytics module.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { createLocalStorageAdapter } from "./application/local-storage-adapter";
|
|
6
|
+
export * from "./domain/operations/search-analytics-operations";
|
|
7
|
+
export * from "./ports/search-analytics-port";
|
|
8
|
+
export * from "./types/search-analytics";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port for search analytics operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SearchEvent } from "../types/search-analytics";
|
|
6
|
+
|
|
7
|
+
export type SearchAnalyticsPort = {
|
|
8
|
+
/**
|
|
9
|
+
* Track search event.
|
|
10
|
+
*/
|
|
11
|
+
readonly trackEvent: (event: SearchEvent, config: unknown) => Promise<void>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get search analytics data.
|
|
15
|
+
*/
|
|
16
|
+
readonly getAnalytics: (
|
|
17
|
+
dateRange: unknown,
|
|
18
|
+
config: unknown,
|
|
19
|
+
) => Promise<readonly SearchEvent[]>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search analytics types for tracking search behavior.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type SearchAnalyticsConfig = {
|
|
6
|
+
readonly enabled: boolean;
|
|
7
|
+
readonly trackQueries: boolean;
|
|
8
|
+
readonly trackClicks: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SearchEvent = {
|
|
12
|
+
readonly type: "query" | "click" | "no-results";
|
|
13
|
+
readonly query: string;
|
|
14
|
+
readonly timestamp: string;
|
|
15
|
+
readonly resultCount?: number;
|
|
16
|
+
};
|