specra 0.2.6 → 0.2.7

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.
Files changed (43) hide show
  1. package/config/specra.config.schema.json +23 -0
  2. package/dist/category.d.ts +1 -1
  3. package/dist/category.js +4 -2
  4. package/dist/components/docs/Breadcrumb.svelte +11 -4
  5. package/dist/components/docs/Breadcrumb.svelte.d.ts +1 -0
  6. package/dist/components/docs/CategoryIndex.svelte +7 -2
  7. package/dist/components/docs/CategoryIndex.svelte.d.ts +1 -0
  8. package/dist/components/docs/DocLayout.svelte +9 -8
  9. package/dist/components/docs/DocLayout.svelte.d.ts +1 -0
  10. package/dist/components/docs/DocNavigation.svelte +10 -3
  11. package/dist/components/docs/DocNavigation.svelte.d.ts +1 -0
  12. package/dist/components/docs/Header.svelte +17 -1
  13. package/dist/components/docs/Header.svelte.d.ts +10 -0
  14. package/dist/components/docs/MobileDocLayout.svelte +5 -1
  15. package/dist/components/docs/MobileDocLayout.svelte.d.ts +1 -0
  16. package/dist/components/docs/MobileSidebar.svelte +3 -1
  17. package/dist/components/docs/MobileSidebar.svelte.d.ts +1 -0
  18. package/dist/components/docs/MobileSidebarWrapper.svelte +3 -1
  19. package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +1 -0
  20. package/dist/components/docs/ProductSwitcher.svelte +175 -0
  21. package/dist/components/docs/ProductSwitcher.svelte.d.ts +28 -0
  22. package/dist/components/docs/Sidebar.svelte +3 -1
  23. package/dist/components/docs/Sidebar.svelte.d.ts +1 -0
  24. package/dist/components/docs/SidebarMenuItems.svelte +16 -8
  25. package/dist/components/docs/SidebarMenuItems.svelte.d.ts +1 -0
  26. package/dist/components/docs/TabGroups.svelte +9 -2
  27. package/dist/components/docs/TabGroups.svelte.d.ts +1 -0
  28. package/dist/components/docs/VersionSwitcher.svelte +1 -1
  29. package/dist/components/docs/index.d.ts +1 -0
  30. package/dist/components/docs/index.js +1 -0
  31. package/dist/components/ui/Button.svelte.d.ts +1 -1
  32. package/dist/config.d.ts +1 -1
  33. package/dist/config.schema.json +18 -0
  34. package/dist/config.server.d.ts +35 -7
  35. package/dist/config.server.js +185 -23
  36. package/dist/config.types.d.ts +46 -0
  37. package/dist/mdx-cache.d.ts +3 -6
  38. package/dist/mdx-cache.js +36 -8
  39. package/dist/mdx.d.ts +8 -3
  40. package/dist/mdx.js +40 -9
  41. package/dist/redirects.d.ts +2 -1
  42. package/dist/redirects.js +37 -11
  43. package/package.json +1 -1
package/dist/mdx-cache.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * invalidated automatically when files change.
7
7
  */
8
8
  import { getVersions, getAllDocs, getDocBySlug } from './mdx';
9
+ import { clearProductCaches } from './config.server';
9
10
  import { watch } from 'fs';
10
11
  import { join } from 'path';
11
12
  import { PerfTimer, logCacheOperation } from './dev-utils';
@@ -36,6 +37,11 @@ function initializeWatchers() {
36
37
  return;
37
38
  // Invalidate relevant caches when MDX or JSON files change
38
39
  if (filename.endsWith('.mdx') || filename.endsWith('.json')) {
40
+ // Clear product caches when _product_.json changes
41
+ if (filename.endsWith('_product_.json')) {
42
+ clearProductCaches();
43
+ console.log(`[MDX Cache] Product cache invalidated: ${filename}`);
44
+ }
39
45
  // Extract version from path
40
46
  const parts = filename.split(/[/\\]/);
41
47
  const version = parts[0];
@@ -52,6 +58,7 @@ function initializeWatchers() {
52
58
  // Clear versions cache if directory structure changed
53
59
  if (eventType === 'rename') {
54
60
  versionsCache.data = null;
61
+ clearProductCaches();
55
62
  }
56
63
  console.log(`[MDX Cache] Invalidated cache for: ${filename}`);
57
64
  }
@@ -69,11 +76,28 @@ function isCacheValid(timestamp) {
69
76
  return Date.now() - timestamp < CACHE_TTL;
70
77
  }
71
78
  /**
72
- * Cached version of getVersions()
79
+ * Cached version of getVersions().
80
+ * When product is provided, uses a separate cache key per product.
73
81
  */
74
- export function getCachedVersions() {
82
+ const productVersionsCache = new Map();
83
+ export function getCachedVersions(product) {
75
84
  // Initialize watchers on first use
76
85
  initializeWatchers();
86
+ // For product-scoped versions, use a separate cache
87
+ if (product && product !== "_default_") {
88
+ const cached = productVersionsCache.get(product);
89
+ if (cached && isCacheValid(cached.timestamp)) {
90
+ logCacheOperation('hit', `versions:${product}`);
91
+ return cached.data;
92
+ }
93
+ logCacheOperation('miss', `versions:${product}`);
94
+ const timer = new PerfTimer(`getVersions(${product})`);
95
+ const versions = getVersions(product);
96
+ timer.end();
97
+ productVersionsCache.set(product, { data: versions, timestamp: Date.now() });
98
+ return versions;
99
+ }
100
+ // Default product — original cache
77
101
  if (versionsCache.data && isCacheValid(versionsCache.timestamp)) {
78
102
  logCacheOperation('hit', 'versions');
79
103
  return versionsCache.data;
@@ -89,10 +113,11 @@ export function getCachedVersions() {
89
113
  /**
90
114
  * Cached version of getAllDocs()
91
115
  */
92
- export async function getCachedAllDocs(version = 'v1.0.0', locale) {
116
+ export async function getCachedAllDocs(version = 'v1.0.0', locale, product) {
93
117
  // Initialize watchers on first use
94
118
  initializeWatchers();
95
- const cacheKey = locale ? `${version}:${locale}` : version;
119
+ const productKey = (product && product !== "_default_") ? product : "_default_";
120
+ const cacheKey = locale ? `${productKey}:${version}:${locale}` : `${productKey}:${version}`;
96
121
  const cached = allDocsCache.get(cacheKey);
97
122
  if (cached && isCacheValid(cached.timestamp)) {
98
123
  logCacheOperation('hit', `getAllDocs:${cacheKey}`);
@@ -100,7 +125,7 @@ export async function getCachedAllDocs(version = 'v1.0.0', locale) {
100
125
  }
101
126
  logCacheOperation('miss', `getAllDocs:${cacheKey}`);
102
127
  const timer = new PerfTimer(`getAllDocs(${cacheKey})`);
103
- const docs = await getAllDocs(version, locale);
128
+ const docs = await getAllDocs(version, locale, product);
104
129
  timer.end();
105
130
  allDocsCache.set(cacheKey, {
106
131
  data: docs,
@@ -111,10 +136,11 @@ export async function getCachedAllDocs(version = 'v1.0.0', locale) {
111
136
  /**
112
137
  * Cached version of getDocBySlug()
113
138
  */
114
- export async function getCachedDocBySlug(slug, version = 'v1.0.0') {
139
+ export async function getCachedDocBySlug(slug, version = 'v1.0.0', product) {
115
140
  // Initialize watchers on first use
116
141
  initializeWatchers();
117
- const cacheKey = `${version}:${slug}`;
142
+ const productKey = (product && product !== "_default_") ? product : "_default_";
143
+ const cacheKey = `${productKey}:${version}:${slug}`;
118
144
  const cached = docBySlugCache.get(cacheKey);
119
145
  if (cached && isCacheValid(cached.timestamp)) {
120
146
  logCacheOperation('hit', `getDocBySlug:${cacheKey}`);
@@ -122,7 +148,7 @@ export async function getCachedDocBySlug(slug, version = 'v1.0.0') {
122
148
  }
123
149
  logCacheOperation('miss', `getDocBySlug:${cacheKey}`);
124
150
  const timer = new PerfTimer(`getDocBySlug(${slug})`);
125
- const doc = await getDocBySlug(slug, version);
151
+ const doc = await getDocBySlug(slug, version, undefined, product);
126
152
  timer.end();
127
153
  docBySlugCache.set(cacheKey, {
128
154
  data: doc,
@@ -136,8 +162,10 @@ export async function getCachedDocBySlug(slug, version = 'v1.0.0') {
136
162
  */
137
163
  export function clearAllCaches() {
138
164
  versionsCache.data = null;
165
+ productVersionsCache.clear();
139
166
  allDocsCache.clear();
140
167
  docBySlugCache.clear();
168
+ clearProductCaches();
141
169
  console.log('[MDX Cache] All caches cleared');
142
170
  }
143
171
  /**
package/dist/mdx.d.ts CHANGED
@@ -58,10 +58,15 @@ export interface TocItem {
58
58
  title: string;
59
59
  level: number;
60
60
  }
61
- export declare function getVersions(): string[];
61
+ export declare function getVersions(product?: string): string[];
62
+ /**
63
+ * Get versions scoped to a specific product.
64
+ * Convenience wrapper around getVersions() for product-aware code.
65
+ */
66
+ export declare function getProductVersions(product: string): string[];
62
67
  export declare function getI18nConfig(): I18nConfig | null;
63
- export declare function getDocBySlug(slug: string, version?: string, locale?: string): Promise<Doc | null>;
64
- export declare function getAllDocs(version?: string, locale?: string): Doc[];
68
+ export declare function getDocBySlug(slug: string, version?: string, locale?: string, product?: string): Promise<Doc | null>;
69
+ export declare function getAllDocs(version?: string, locale?: string, product?: string): Doc[];
65
70
  export declare function getAdjacentDocs(currentSlug: string, allDocs: Doc[]): {
66
71
  previous?: Doc;
67
72
  next?: Doc;
package/dist/mdx.js CHANGED
@@ -16,6 +16,17 @@ import { sortSidebarItems, sortSidebarGroups, buildSidebarStructure } from "./si
16
16
  import { sanitizePath, validatePathWithinDirectory, validateMDXSecurity } from "./mdx-security";
17
17
  import { getConfig } from "./config";
18
18
  const DOCS_DIR = path.join(process.cwd(), "docs");
19
+ /**
20
+ * Resolve the docs directory for a given version and optional product.
21
+ * - Default product or omitted: docs/{version}/
22
+ * - Named product: docs/{product}/{version}/
23
+ */
24
+ function resolveDocsPath(version, product) {
25
+ if (product && product !== "_default_") {
26
+ return path.join(DOCS_DIR, product, version);
27
+ }
28
+ return path.join(DOCS_DIR, version);
29
+ }
19
30
  /**
20
31
  * Map of lowercased HTML tag names to PascalCase component names.
21
32
  * When rehype processes MDX, it lowercases all custom tags.
@@ -981,15 +992,35 @@ function calculateReadingTime(content) {
981
992
  const minutes = Math.ceil(words / 200);
982
993
  return { minutes, words };
983
994
  }
984
- export function getVersions() {
995
+ export function getVersions(product) {
985
996
  try {
986
- const versions = fs.readdirSync(DOCS_DIR);
987
- return versions.filter((v) => fs.statSync(path.join(DOCS_DIR, v)).isDirectory());
997
+ const baseDir = (product && product !== "_default_")
998
+ ? path.join(DOCS_DIR, product)
999
+ : DOCS_DIR;
1000
+ const entries = fs.readdirSync(baseDir);
1001
+ return entries.filter((v) => {
1002
+ if (!fs.statSync(path.join(baseDir, v)).isDirectory())
1003
+ return false;
1004
+ // In the default product's base dir, skip product directories (those with _product_.json)
1005
+ if (!product || product === "_default_") {
1006
+ const hasProductJson = fs.existsSync(path.join(baseDir, v, "_product_.json"));
1007
+ if (hasProductJson)
1008
+ return false;
1009
+ }
1010
+ return true;
1011
+ });
988
1012
  }
989
1013
  catch (error) {
990
1014
  return ["v1.0.0"];
991
1015
  }
992
1016
  }
1017
+ /**
1018
+ * Get versions scoped to a specific product.
1019
+ * Convenience wrapper around getVersions() for product-aware code.
1020
+ */
1021
+ export function getProductVersions(product) {
1022
+ return getVersions(product);
1023
+ }
993
1024
  /**
994
1025
  * Recursively find all MDX files in a directory
995
1026
  */
@@ -1094,7 +1125,7 @@ export function getI18nConfig() {
1094
1125
  }
1095
1126
  return i18n;
1096
1127
  }
1097
- export async function getDocBySlug(slug, version = "v1.0.0", locale) {
1128
+ export async function getDocBySlug(slug, version = "v1.0.0", locale, product) {
1098
1129
  try {
1099
1130
  // Security: Sanitize and validate slug
1100
1131
  const sanitizedVersion = sanitizePath(version);
@@ -1117,8 +1148,8 @@ export async function getDocBySlug(slug, version = "v1.0.0", locale) {
1117
1148
  // Try finding the file in this order:
1118
1149
  // 1. Localized extension: slug.locale.mdx (e.g. guide.fr.mdx)
1119
1150
  // 2. Default file: slug.mdx (only if using default locale and configured to fallback or strictly default)
1120
- // Construct potential paths
1121
- const basePath = path.join(DOCS_DIR, sanitizedVersion);
1151
+ // Construct potential paths — product-aware
1152
+ const basePath = resolveDocsPath(sanitizedVersion, product);
1122
1153
  let result = null;
1123
1154
  // 1. Try localized file extension
1124
1155
  if (targetLocale) {
@@ -1156,9 +1187,9 @@ export async function getDocBySlug(slug, version = "v1.0.0", locale) {
1156
1187
  return null;
1157
1188
  }
1158
1189
  }
1159
- export function getAllDocs(version = "v1.0.0", locale) {
1190
+ export function getAllDocs(version = "v1.0.0", locale, product) {
1160
1191
  try {
1161
- const versionDir = path.join(DOCS_DIR, version);
1192
+ const versionDir = resolveDocsPath(version, product);
1162
1193
  if (!fs.existsSync(versionDir)) {
1163
1194
  return [];
1164
1195
  }
@@ -1166,7 +1197,7 @@ export function getAllDocs(version = "v1.0.0", locale) {
1166
1197
  const i18nConfig = getI18nConfig();
1167
1198
  const targetLocale = locale || i18nConfig?.defaultLocale || 'en';
1168
1199
  const mdxFiles = findMdxFiles(versionDir);
1169
- const categoryConfigs = getAllCategoryConfigs(version);
1200
+ const categoryConfigs = getAllCategoryConfigs(version, product);
1170
1201
  const docs = mdxFiles.map((file) => {
1171
1202
  // file contains path relative to version dir, e.g. "getting-started/intro.mdx" or "intro.fr.mdx"
1172
1203
  let originalFilePath = file.replace(/\.mdx$/, "");
@@ -3,7 +3,8 @@ export interface RedirectMapping {
3
3
  to: string;
4
4
  }
5
5
  /**
6
- * Build redirect mappings from all docs' redirect_from frontmatter
6
+ * Build redirect mappings from all docs' redirect_from frontmatter.
7
+ * In multi-product mode, generates product-aware redirect URLs.
7
8
  */
8
9
  export declare function buildRedirectMappings(): Promise<RedirectMapping[]>;
9
10
  /**
package/dist/redirects.js CHANGED
@@ -1,19 +1,45 @@
1
1
  import { getAllDocs, getVersions } from "./mdx";
2
+ import { getProducts } from "./config.server";
2
3
  /**
3
- * Build redirect mappings from all docs' redirect_from frontmatter
4
+ * Build redirect mappings from all docs' redirect_from frontmatter.
5
+ * In multi-product mode, generates product-aware redirect URLs.
4
6
  */
5
7
  export async function buildRedirectMappings() {
6
- const versions = getVersions();
7
8
  const redirects = [];
8
- for (const version of versions) {
9
- const docs = await getAllDocs(version);
10
- for (const doc of docs) {
11
- if (doc.meta.redirect_from && Array.isArray(doc.meta.redirect_from)) {
12
- for (const oldPath of doc.meta.redirect_from) {
13
- redirects.push({
14
- from: oldPath,
15
- to: `/docs/${version}/${doc.slug}`,
16
- });
9
+ const products = getProducts();
10
+ if (products.length === 0) {
11
+ // Single-product mode current behavior
12
+ const versions = getVersions();
13
+ for (const version of versions) {
14
+ const docs = await getAllDocs(version);
15
+ for (const doc of docs) {
16
+ if (doc.meta.redirect_from && Array.isArray(doc.meta.redirect_from)) {
17
+ for (const oldPath of doc.meta.redirect_from) {
18
+ redirects.push({
19
+ from: oldPath,
20
+ to: `/docs/${version}/${doc.slug}`,
21
+ });
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ else {
28
+ // Multi-product mode
29
+ for (const product of products) {
30
+ const versions = getVersions(product.isDefault ? undefined : product.slug);
31
+ const urlPrefix = product.isDefault ? "/docs" : `/docs/${product.slug}`;
32
+ for (const version of versions) {
33
+ const docs = await getAllDocs(version, undefined, product.isDefault ? undefined : product.slug);
34
+ for (const doc of docs) {
35
+ if (doc.meta.redirect_from && Array.isArray(doc.meta.redirect_from)) {
36
+ for (const oldPath of doc.meta.redirect_from) {
37
+ redirects.push({
38
+ from: oldPath,
39
+ to: `${urlPrefix}/${version}/${doc.slug}`,
40
+ });
41
+ }
42
+ }
17
43
  }
18
44
  }
19
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specra",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "A modern documentation library for SvelteKit with built-in versioning, API reference generation, full-text search, and MDX support",
5
5
  "svelte": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",