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.
- package/config/specra.config.schema.json +23 -0
- package/dist/category.d.ts +1 -1
- package/dist/category.js +4 -2
- package/dist/components/docs/Breadcrumb.svelte +11 -4
- package/dist/components/docs/Breadcrumb.svelte.d.ts +1 -0
- package/dist/components/docs/CategoryIndex.svelte +7 -2
- package/dist/components/docs/CategoryIndex.svelte.d.ts +1 -0
- package/dist/components/docs/DocLayout.svelte +9 -8
- package/dist/components/docs/DocLayout.svelte.d.ts +1 -0
- package/dist/components/docs/DocNavigation.svelte +10 -3
- package/dist/components/docs/DocNavigation.svelte.d.ts +1 -0
- package/dist/components/docs/Header.svelte +17 -1
- package/dist/components/docs/Header.svelte.d.ts +10 -0
- package/dist/components/docs/MobileDocLayout.svelte +5 -1
- package/dist/components/docs/MobileDocLayout.svelte.d.ts +1 -0
- package/dist/components/docs/MobileSidebar.svelte +3 -1
- package/dist/components/docs/MobileSidebar.svelte.d.ts +1 -0
- package/dist/components/docs/MobileSidebarWrapper.svelte +3 -1
- package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +1 -0
- package/dist/components/docs/ProductSwitcher.svelte +175 -0
- package/dist/components/docs/ProductSwitcher.svelte.d.ts +28 -0
- package/dist/components/docs/Sidebar.svelte +3 -1
- package/dist/components/docs/Sidebar.svelte.d.ts +1 -0
- package/dist/components/docs/SidebarMenuItems.svelte +16 -8
- package/dist/components/docs/SidebarMenuItems.svelte.d.ts +1 -0
- package/dist/components/docs/TabGroups.svelte +9 -2
- package/dist/components/docs/TabGroups.svelte.d.ts +1 -0
- package/dist/components/docs/VersionSwitcher.svelte +1 -1
- package/dist/components/docs/index.d.ts +1 -0
- package/dist/components/docs/index.js +1 -0
- package/dist/components/ui/Button.svelte.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.schema.json +18 -0
- package/dist/config.server.d.ts +35 -7
- package/dist/config.server.js +185 -23
- package/dist/config.types.d.ts +46 -0
- package/dist/mdx-cache.d.ts +3 -6
- package/dist/mdx-cache.js +36 -8
- package/dist/mdx.d.ts +8 -3
- package/dist/mdx.js +40 -9
- package/dist/redirects.d.ts +2 -1
- package/dist/redirects.js +37 -11
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
987
|
-
|
|
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 =
|
|
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 =
|
|
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$/, "");
|
package/dist/redirects.d.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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",
|