@ox-content/vite-plugin 2.32.0 → 2.34.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/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ import * as fs from "node:fs/promises";
19
19
  import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
20
20
  import { tmpdir } from "node:os";
21
21
  import { promisify } from "node:util";
22
- import { execFile } from "node:child_process";
22
+ import { execFile, spawn } from "node:child_process";
23
23
  import * as fs$2 from "fs/promises";
24
24
  import * as crypto from "crypto";
25
25
  import * as fs$1 from "fs";
@@ -837,7 +837,8 @@ async function extractDocs(srcDirs, options) {
837
837
  return extractDocsFromEntryPoints(options.entryPoints, {
838
838
  root: process.cwd(),
839
839
  private: options.private,
840
- internal: options.internal
840
+ internal: options.internal,
841
+ typeParameters: options.typeParameters
841
842
  }).map((doc) => ({
842
843
  file: doc.file,
843
844
  entries: doc.entries
@@ -845,7 +846,7 @@ async function extractDocs(srcDirs, options) {
845
846
  }
846
847
  const extractDocsFromDirectories = napi.extractDocsFromDirectories;
847
848
  if (!extractDocsFromDirectories) throw new Error("[ox-content] extractDocsFromDirectories is not available from @ox-content/napi.");
848
- return extractDocsFromDirectories(srcDirs, options.include, options.exclude, options.private, options.internal).map((doc) => ({
849
+ return extractDocsFromDirectories(srcDirs, options.include, options.exclude, options.private, options.internal, options.typeParameters).map((doc) => ({
849
850
  file: doc.file,
850
851
  entries: doc.entries
851
852
  }));
@@ -926,6 +927,7 @@ function resolveDocsOptions(options) {
926
927
  basePath: opts.basePath,
927
928
  pathStrategy: opts.pathStrategy ?? "flat",
928
929
  renderStyle: opts.renderStyle ?? "html",
930
+ typeParameters: opts.typeParameters ?? false,
929
931
  generateNav: opts.generateNav ?? true
930
932
  };
931
933
  }
@@ -3006,6 +3008,337 @@ function generateI18nModule(options, root) {
3006
3008
  throw new Error("[ox-content:i18n] @ox-content/napi does not expose generateI18nModule. Please rebuild the NAPI package.");
3007
3009
  }
3008
3010
  //#endregion
3011
+ //#region src/docs-tests.ts
3012
+ var DocsTestRunError = class extends Error {
3013
+ result;
3014
+ constructor(result) {
3015
+ const command = [result.command, ...result.args].join(" ");
3016
+ super(`[ox-content] Docs tests failed with exit code ${result.exitCode}: ${command}`);
3017
+ this.name = "DocsTestRunError";
3018
+ this.result = result;
3019
+ }
3020
+ };
3021
+ async function collectDocsTests(options) {
3022
+ const cwd = path.resolve(options.cwd ?? process.cwd());
3023
+ if ((options.source ?? "markdown") === "jsdoc") return collectJsdocDocsTests(options, cwd);
3024
+ return collectMarkdownDocsTests(options, cwd);
3025
+ }
3026
+ async function collectMarkdownDocsTests(options, cwd) {
3027
+ const include = toArray(options.include);
3028
+ const ignore = toArray(options.ignore);
3029
+ const files = /* @__PURE__ */ new Map();
3030
+ if (include.length === 0) throw new Error("[ox-content] Docs test include patterns are required for markdown sources.");
3031
+ for (const pattern of include) {
3032
+ const matches = await glob(pattern, {
3033
+ absolute: true,
3034
+ cwd,
3035
+ ignore,
3036
+ nodir: true
3037
+ });
3038
+ for (const filePath of matches) {
3039
+ const absolutePath = path.resolve(filePath);
3040
+ files.set(absolutePath, normalizePath$1(path.relative(cwd, absolutePath)));
3041
+ }
3042
+ }
3043
+ const blocks = [];
3044
+ let index = 0;
3045
+ for (const [sourcePath, relativePath] of [...files.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
3046
+ const extracted = await extractDocsTests(await fs.readFile(sourcePath, "utf-8"), {
3047
+ languages: options.languages,
3048
+ requireMeta: options.requireMeta
3049
+ });
3050
+ for (const block of extracted) {
3051
+ blocks.push({
3052
+ ...block,
3053
+ sourcePath,
3054
+ relativePath,
3055
+ index
3056
+ });
3057
+ index += 1;
3058
+ }
3059
+ }
3060
+ return blocks;
3061
+ }
3062
+ async function collectJsdocDocsTests(options, cwd) {
3063
+ const docsOptions = resolveJsdocDocsOptions(options, cwd);
3064
+ const docs = await extractDocs(docsOptions.src, docsOptions);
3065
+ const blocks = [];
3066
+ let index = 0;
3067
+ for (const doc of sortDocs(docs)) for (const entry of sortEntries(doc.entries)) for (const example of entry.examples ?? []) {
3068
+ const extracted = await extractDocsTests(example, {
3069
+ languages: options.languages,
3070
+ requireMeta: options.requireMeta
3071
+ });
3072
+ const sourcePath = resolveEntrySourcePath(entry, doc, cwd);
3073
+ const relativePath = relativeSourcePath(cwd, sourcePath);
3074
+ for (const block of extracted) {
3075
+ blocks.push({
3076
+ ...block,
3077
+ sourcePath,
3078
+ relativePath,
3079
+ startLine: entry.line,
3080
+ endLine: entry.endLine,
3081
+ index
3082
+ });
3083
+ index += 1;
3084
+ }
3085
+ }
3086
+ return blocks;
3087
+ }
3088
+ function resolveJsdocDocsOptions(options, cwd) {
3089
+ const docsOptions = { ...options.docs };
3090
+ if (options.src !== void 0) docsOptions.src = toArray(options.src);
3091
+ if (options.include !== void 0) docsOptions.include = toArray(options.include);
3092
+ if (options.ignore !== void 0) docsOptions.exclude = toArray(options.ignore);
3093
+ const resolved = resolveDocsOptions(docsOptions);
3094
+ return {
3095
+ ...resolved,
3096
+ src: resolved.src.map((sourceDir) => path.resolve(cwd, sourceDir)),
3097
+ entryPoints: resolved.entryPoints?.map((entryPoint) => ({
3098
+ ...entryPoint,
3099
+ path: path.resolve(cwd, entryPoint.path)
3100
+ }))
3101
+ };
3102
+ }
3103
+ function sortDocs(docs) {
3104
+ return [...docs].sort((left, right) => left.file.localeCompare(right.file));
3105
+ }
3106
+ function sortEntries(entries) {
3107
+ return [...entries].sort((left, right) => {
3108
+ const byFile = left.file.localeCompare(right.file);
3109
+ if (byFile !== 0) return byFile;
3110
+ const byLine = left.line - right.line;
3111
+ if (byLine !== 0) return byLine;
3112
+ return left.name.localeCompare(right.name);
3113
+ });
3114
+ }
3115
+ function resolveEntrySourcePath(entry, doc, cwd) {
3116
+ const sourcePath = entry.file || doc.file;
3117
+ return path.isAbsolute(sourcePath) ? path.resolve(sourcePath) : path.resolve(cwd, sourcePath);
3118
+ }
3119
+ function relativeSourcePath(cwd, sourcePath) {
3120
+ const relativePath = path.relative(cwd, sourcePath);
3121
+ if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) return normalizePath$1(relativePath);
3122
+ return normalizePath$1(sourcePath);
3123
+ }
3124
+ async function writeDocsTestFiles(options) {
3125
+ const cwd = path.resolve(options.cwd ?? process.cwd());
3126
+ const generatedDir = path.resolve(cwd, options.generatedDir ?? ".cache/ox-content-docs-tests");
3127
+ const clean = options.clean ?? true;
3128
+ const blocks = await collectDocsTests({
3129
+ ...options,
3130
+ cwd
3131
+ });
3132
+ if (clean) await fs.rm(generatedDir, {
3133
+ recursive: true,
3134
+ force: true
3135
+ });
3136
+ await fs.mkdir(generatedDir, { recursive: true });
3137
+ return {
3138
+ cwd,
3139
+ generatedDir,
3140
+ blocks,
3141
+ files: await Promise.all(blocks.map(async (block) => {
3142
+ const filePath = path.join(generatedDir, docsTestFileName(block));
3143
+ await fs.writeFile(filePath, renderDocsTestFile(block, options), "utf-8");
3144
+ return {
3145
+ filePath,
3146
+ sourcePath: block.sourcePath,
3147
+ relativePath: block.relativePath,
3148
+ startLine: block.startLine,
3149
+ endLine: block.endLine,
3150
+ language: block.language
3151
+ };
3152
+ }))
3153
+ };
3154
+ }
3155
+ async function runDocsTests(options) {
3156
+ const writeResult = await writeDocsTestFiles(options);
3157
+ const command = options.vitestCommand ?? "vitest";
3158
+ const leadingArgs = options.vitestArgs ?? ["run"];
3159
+ const fileArgs = writeResult.files.map((file) => file.filePath);
3160
+ const args = [...leadingArgs, ...fileArgs];
3161
+ if (fileArgs.length === 0) {
3162
+ if (options.allowEmpty) return {
3163
+ ...writeResult,
3164
+ command,
3165
+ args,
3166
+ exitCode: 0,
3167
+ stdout: "",
3168
+ stderr: ""
3169
+ };
3170
+ throw new Error("[ox-content] No runnable docs test blocks were found.");
3171
+ }
3172
+ const result = await runCommand(command, args, {
3173
+ cwd: writeResult.cwd,
3174
+ env: mergeEnv(options.env)
3175
+ });
3176
+ const runResult = {
3177
+ ...writeResult,
3178
+ command,
3179
+ args,
3180
+ ...result
3181
+ };
3182
+ if (runResult.exitCode !== 0) throw new DocsTestRunError(runResult);
3183
+ return runResult;
3184
+ }
3185
+ function renderDocsTestFile(block, options) {
3186
+ const parts = [
3187
+ "// Generated by @ox-content/vite-plugin docs test harness.",
3188
+ `// Source: ${block.relativePath}:${block.startLine}-${block.endLine}`,
3189
+ ""
3190
+ ];
3191
+ const setupCode = options.setupCode?.trimEnd();
3192
+ const code = rewriteImports(block.code.trimEnd(), options.importRewrites);
3193
+ if (setupCode) parts.push(setupCode, "");
3194
+ if ((options.executionMode ?? "test") === "module") {
3195
+ parts.push(code, "");
3196
+ return parts.join("\n");
3197
+ }
3198
+ const { imports, body } = partitionImports(code);
3199
+ parts.push(`import { test } from ${JSON.stringify(rewriteSpecifier(options.testImport ?? "vitest", options.importRewrites))};`);
3200
+ if (imports.length > 0) parts.push(...imports);
3201
+ parts.push("", `test(${JSON.stringify(`${block.relativePath}:${block.startLine}`)}, async () => {`);
3202
+ if (body.trim().length > 0) parts.push(indentCode(body.trimEnd()));
3203
+ parts.push("});", "");
3204
+ return parts.join("\n");
3205
+ }
3206
+ function partitionImports(source) {
3207
+ const imports = [];
3208
+ const body = [];
3209
+ const lines = source.split(/\r?\n/);
3210
+ let currentImport;
3211
+ for (const line of lines) {
3212
+ if (currentImport) {
3213
+ currentImport.push(line);
3214
+ if (endsImportDeclaration(line)) {
3215
+ imports.push(currentImport.join("\n"));
3216
+ currentImport = void 0;
3217
+ }
3218
+ continue;
3219
+ }
3220
+ if (startsStaticImport(line)) {
3221
+ if (endsImportDeclaration(line)) imports.push(line);
3222
+ else currentImport = [line];
3223
+ continue;
3224
+ }
3225
+ body.push(line);
3226
+ }
3227
+ if (currentImport) body.push(...currentImport);
3228
+ return {
3229
+ imports,
3230
+ body: body.join("\n")
3231
+ };
3232
+ }
3233
+ function startsStaticImport(line) {
3234
+ const trimmed = line.trimStart();
3235
+ return trimmed.startsWith("import ") && !trimmed.startsWith("import(");
3236
+ }
3237
+ function endsImportDeclaration(line) {
3238
+ const trimmed = line.trim();
3239
+ return trimmed.endsWith(";") || /^import\s+["'][^"']+["']$/.test(trimmed) || /\sfrom\s+["'][^"']+["']$/.test(trimmed);
3240
+ }
3241
+ function indentCode(source) {
3242
+ return source.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
3243
+ }
3244
+ function rewriteImports(source, rewrites) {
3245
+ if (!rewrites) return source;
3246
+ let result = source;
3247
+ for (const [from, to] of Object.entries(rewrites)) {
3248
+ const escaped = escapeRegExp(from);
3249
+ result = result.replace(new RegExp(`(from\\s+["'])${escaped}(["'])`, "g"), `$1${to}$2`).replace(new RegExp(`(import\\s+["'])${escaped}(["'])`, "g"), `$1${to}$2`).replace(new RegExp(`(import\\(\\s*["'])${escaped}(["']\\s*\\))`, "g"), `$1${to}$2`);
3250
+ }
3251
+ return result;
3252
+ }
3253
+ /**
3254
+ * Applies the same import rewrite table to harness-owned imports.
3255
+ *
3256
+ * @internal
3257
+ * @example
3258
+ * ```ts docs-test
3259
+ * import { expect } from "vitest";
3260
+ * import { extractDocsTests } from "../../src/code-blocks";
3261
+ *
3262
+ * const markdown = [
3263
+ * "```ts docs-test",
3264
+ * "expect(1 + 1).toBe(2);",
3265
+ * "```",
3266
+ * ].join("\n");
3267
+ *
3268
+ * const blocks = await extractDocsTests(markdown);
3269
+ *
3270
+ * expect(blocks).toHaveLength(1);
3271
+ * expect(blocks[0]?.code).toContain("1 + 1");
3272
+ * ```
3273
+ */
3274
+ function rewriteSpecifier(specifier, rewrites) {
3275
+ return rewrites?.[specifier] ?? specifier;
3276
+ }
3277
+ function escapeRegExp(value) {
3278
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3279
+ }
3280
+ function docsTestFileName(block) {
3281
+ return `${block.relativePath.replace(/^\.\//, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "docs-test"}-L${block.startLine}-${block.index + 1}.test.${extensionForLanguage(block.language)}`;
3282
+ }
3283
+ function extensionForLanguage(language) {
3284
+ switch (language.toLowerCase()) {
3285
+ case "jsx": return "jsx";
3286
+ case "tsx": return "tsx";
3287
+ case "mjs": return "mjs";
3288
+ case "mts": return "mts";
3289
+ case "js": return "js";
3290
+ default: return "ts";
3291
+ }
3292
+ }
3293
+ function toArray(value) {
3294
+ if (!value) return [];
3295
+ return Array.isArray(value) ? value : [value];
3296
+ }
3297
+ function normalizePath$1(value) {
3298
+ return value.split(path.sep).join("/");
3299
+ }
3300
+ function mergeEnv(overrides) {
3301
+ const env = { ...process.env };
3302
+ for (const [key, value] of Object.entries(overrides ?? {})) if (value === void 0) delete env[key];
3303
+ else env[key] = value;
3304
+ return env;
3305
+ }
3306
+ function runCommand(command, args, options) {
3307
+ return new Promise((resolve, reject) => {
3308
+ const child = spawn(command, args, {
3309
+ cwd: options.cwd,
3310
+ env: options.env,
3311
+ stdio: [
3312
+ "ignore",
3313
+ "pipe",
3314
+ "pipe"
3315
+ ]
3316
+ });
3317
+ let stdout = "";
3318
+ let stderr = "";
3319
+ if (child.stdout) {
3320
+ child.stdout.setEncoding("utf-8");
3321
+ child.stdout.on("data", (chunk) => {
3322
+ stdout += chunk;
3323
+ });
3324
+ }
3325
+ if (child.stderr) {
3326
+ child.stderr.setEncoding("utf-8");
3327
+ child.stderr.on("data", (chunk) => {
3328
+ stderr += chunk;
3329
+ });
3330
+ }
3331
+ child.on("error", reject);
3332
+ child.on("close", (exitCode) => {
3333
+ resolve({
3334
+ exitCode: exitCode ?? 1,
3335
+ stdout,
3336
+ stderr
3337
+ });
3338
+ });
3339
+ });
3340
+ }
3341
+ //#endregion
3009
3342
  //#region src/lint.ts
3010
3343
  const require$1 = createRequire(import.meta.url);
3011
3344
  const SUPPORTED_MARKDOWN_LINT_LANGUAGES = [
@@ -4422,6 +4755,6 @@ function normalizeRuntimeBase(base) {
4422
4755
  return withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
4423
4756
  }
4424
4757
  //#endregion
4425
- export { DEFAULT_HTML_TEMPLATE, DEFAULT_MARKDOWN_EXTENSIONS, DefaultTheme, Fragment, buildSearchIndex, buildSsg, clearRenderContext, collectGitHubRepos, collectGitHubSources, collectOgpUrls, convertVitePressNav, convertVitePressSidebar, createI18nPlugin, createMarkdownEnvironment, createTheme, defaultTheme, defineTheme, each, extractCodeBlocks, extractDocs, extractDocsTests, extractIslandInfo, extractVideoId, fetchGitHubSource, fetchOgpData, fetchRepoData, fromVitePressConfig, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, generateVirtualModule, generateVitePressMigrationConfig, hasIslands, inferType, isMarkdownFilePath, jsx, jsxs, lintCodeBlocks, lintMarkdown, lintMarkdownAsync, lintMarkdownFile, lintMarkdownFiles, mergeThemes, mermaidClientScript, normalizeMarkdownExtensions, normalizeVitePressFrontmatter, oxContent, parseGitHubLineRange, parseGitHubPermalink, prefetchGitHubRepos, prefetchGitHubSources, prefetchOgpData, raw, renderAllPages, renderPage, renderToString, resolveBuiltinEmbedOptions, resolveDocsOptions, resolveI18nOptions, resolveOgImageOptions, resolveSearchOptions, resolveSsgOptions, resolveTheme, setRenderContext, shouldLintMarkdownFile, stripMarkdownExtension, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, typecheckCodeBlocks, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeSearchIndex };
4758
+ export { DEFAULT_HTML_TEMPLATE, DEFAULT_MARKDOWN_EXTENSIONS, DefaultTheme, DocsTestRunError, Fragment, buildSearchIndex, buildSsg, clearRenderContext, collectDocsTests, collectGitHubRepos, collectGitHubSources, collectOgpUrls, convertVitePressNav, convertVitePressSidebar, createI18nPlugin, createMarkdownEnvironment, createTheme, defaultTheme, defineTheme, each, extractCodeBlocks, extractDocs, extractDocsTests, extractIslandInfo, extractVideoId, fetchGitHubSource, fetchOgpData, fetchRepoData, fromVitePressConfig, generateFrontmatterTypes, generateHydrationScript, generateMarkdown, generateOgImages, generateTabsCSS, generateTypes, generateVirtualModule, generateVitePressMigrationConfig, hasIslands, inferType, isMarkdownFilePath, jsx, jsxs, lintCodeBlocks, lintMarkdown, lintMarkdownAsync, lintMarkdownFile, lintMarkdownFiles, mergeThemes, mermaidClientScript, normalizeMarkdownExtensions, normalizeVitePressFrontmatter, oxContent, parseGitHubLineRange, parseGitHubPermalink, prefetchGitHubRepos, prefetchGitHubSources, prefetchOgpData, raw, renderAllPages, renderPage, renderToString, resolveBuiltinEmbedOptions, resolveDocsOptions, resolveI18nOptions, resolveOgImageOptions, resolveSearchOptions, resolveSsgOptions, resolveTheme, runDocsTests, setRenderContext, shouldLintMarkdownFile, stripMarkdownExtension, transformAllPlugins, transformGitHub, transformIslands, transformMarkdown, transformMermaidStatic, transformOgp, transformTabs, transformYouTube, typecheckCodeBlocks, useIsActive, useNav, usePageProps, useRenderContext, useSiteConfig, when, writeDocs, writeDocsTestFiles, writeSearchIndex };
4426
4759
 
4427
4760
  //# sourceMappingURL=index.mjs.map