From c1a5e4184472e1ec370f363e99b227d739951359 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Thu, 27 Aug 2026 08:26:17 +0200 Subject: [PATCH 1/4] chore: release v0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6fada87..c5d0789 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ebuildy/docusaurus-plugin-gitlab", - "version": "0.4.1", + "version": "0.5.0", "description": "MDX extensions to embed GitLab resources in Docusaurus 3 and 4 docs", "license": "MIT", "publishConfig": { From c83773b8602a3594bb5d5ac508ed67bff91cebea Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Thu, 27 Aug 2026 23:21:40 +0200 Subject: [PATCH 2/4] chore: release v0.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5d0789..b50f7d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ebuildy/docusaurus-plugin-gitlab", - "version": "0.5.0", + "version": "0.5.1", "description": "MDX extensions to embed GitLab resources in Docusaurus 3 and 4 docs", "license": "MIT", "publishConfig": { From e4de58a3092490ab7591092acbf1eaa133c010e8 Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Tue, 1 Sep 2026 15:49:44 +0200 Subject: [PATCH 3/4] fix: make gitlabPlugin conform to Docusaurus PluginModule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default export was not assignable to `PluginModule`, and its return value was not a valid `Plugin`, so the registration form documented in README.md and examples/gitlab — `plugins: [[gitlabPlugin, opts]]` — failed to type-check in any TypeScript Docusaurus site: error TS2322: Type '[(context: unknown, options: PluginOptions) => ...]' is not assignable to type 'PluginConfig'. Two causes: 1. `mergeStrategy: { "module.rules": "append" }` inferred the literal as `string`, which is not a webpack-merge `CustomizeRuleString`, so the configureWebpack result violated `ConfigureWebpackResult`. The runtime value was always correct; only the type was wrong. It went unnoticed because the factory has no return-type annotation, so the shape was inferred and never compared against `Plugin`. 2. `options: PluginOptions` is narrower than `PluginModule`'s `options: unknown` and fails contravariantly. Docusaurus's own plugins (e.g. @docusaurus/plugin-sitemap) share this and are likewise not assignable; widening both parameters holds a stricter line than first-party so the documented function form type-checks. Neither was detectable before: @docusaurus/types was not a dependency at any level, and neither example site had a tsconfig.json, so `typecheck` could not see the plugin contract at all. Add two guards: - src/plugin/types.test.ts asserts assignability to `PluginModule`, `Plugin`, and `PluginConfig`. tsconfig.build.json excludes *.test.ts, so the @docusaurus/types import never reaches dist/ — a .d.ts importing it would fail to resolve for pnpm consumers, who do not get the package hoisted (examples/gitlab has to declare it explicitly). - examples/gitlab gains a tsconfig.json and a `typecheck` script, so the consumer-side registration is checked against the real `Config` type. Scoped to docusaurus.config.ts and sidebars.ts: src/theme/* swizzles import `@theme-original/*`, a webpack alias that only resolves against a generated .docusaurus/ dir. Verified both guards fail when the fix is reverted, the example one pointing at the documented line in docusaurus.config.ts. --- CLAUDE.md | 11 ++++ examples/gitlab/package.json | 5 +- examples/gitlab/tsconfig.json | 18 +++++++ package.json | 1 + pnpm-lock.yaml | 95 +++++++++++++++++++++++++++++++++++ src/plugin/index.ts | 18 ++++++- src/plugin/types.test.ts | 50 ++++++++++++++++++ 7 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 examples/gitlab/tsconfig.json create mode 100644 src/plugin/types.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 45e6deb..c47b708 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,17 @@ static HTML. The browser never holds a token or calls the GitLab API. exercises as a second build variant. Keep `configureWebpack` — do NOT add `configureBundler` until a v4 release publishes its signature; a tripwire test in `src/plugin/index.test.ts` enforces this. +- **The default export must stay assignable to Docusaurus's `PluginModule`.** + README and `examples/gitlab` register the plugin in its *function* form + (`plugins: [[gitlabPlugin, opts]]`), which Docusaurus type-checks against + `PluginConfig` — so both parameters must stay `unknown` (a narrower + `options: PluginOptions` fails contravariantly, which is why Docusaurus's own + plugins cannot be passed this way) and the returned object must satisfy + `Plugin`. `src/plugin/types.test.ts` is the compile-time guard; + `examples/gitlab`'s `typecheck` script is the consumer-side one. + `@docusaurus/types` is a **devDependency only** — never import it from a file + that reaches `dist/`, because pnpm consumers cannot resolve it from their own + `node_modules` (`examples/gitlab` has to declare it explicitly). - Prefer the latest versions of libraries. - ESM-first. Intra-package imports use explicit `.js` extensions (e.g. `import { Fallback } from "./Fallback.js"`) — required by the diff --git a/examples/gitlab/package.json b/examples/gitlab/package.json index 1502503..94e68ca 100644 --- a/examples/gitlab/package.json +++ b/examples/gitlab/package.json @@ -5,7 +5,8 @@ "build": "docusaurus build", "start": "docusaurus start", "serve": "docusaurus serve", - "clear": "docusaurus clear" + "clear": "docusaurus clear", + "typecheck": "tsc --noEmit" }, "dependencies": { "@docusaurus/core": "^3.5.0", @@ -16,7 +17,9 @@ "remark-gfm": "^4.0.1" }, "devDependencies": { + "@docusaurus/plugin-content-docs": "^3.10.2", "@docusaurus/tsconfig": "^3.5.0", + "@docusaurus/types": "^3.10.2", "typescript": "^5.4.0" } } diff --git a/examples/gitlab/tsconfig.json b/examples/gitlab/tsconfig.json new file mode 100644 index 0000000..715521f --- /dev/null +++ b/examples/gitlab/tsconfig.json @@ -0,0 +1,18 @@ +{ + // This site registers the plugin in its FUNCTION form + // (`plugins: [[gitlabPlugin, gitlabOptions]]`), which is the form Docusaurus + // actually type-checks against `PluginConfig`. Without a tsconfig here the + // `const config: Config` annotation in docusaurus.config.ts was never checked, + // so a plugin that did not satisfy `PluginModule` still looked fine. + // + // Requires a built `dist/` (the workspace link resolves to it): + // pnpm --filter @ebuildy/docusaurus-plugin-gitlab run build + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "strict": true + }, + // Scoped to the config: `src/theme/*` swizzles import `@theme-original/*`, + // a webpack alias that only resolves against a generated `.docusaurus/` dir, + // so pulling it in here would demand a full site build just to type-check. + "include": ["docusaurus.config.ts", "sidebars.ts"] +} diff --git a/package.json b/package.json index a707b12..66e7761 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ }, "devDependencies": { "@docusaurus/logger": "^3.10.2", + "@docusaurus/types": "^3.10.2", "@eslint/js": "^9", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9327f2e..6d33c92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: '@docusaurus/logger': specifier: ^3.10.2 version: 3.10.2 + '@docusaurus/types': + specifier: ^3.10.2 + version: 3.10.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@eslint/js': specifier: ^9 version: 9.39.5 @@ -180,9 +183,15 @@ importers: specifier: ^4.0.1 version: 4.0.1 devDependencies: + '@docusaurus/plugin-content-docs': + specifier: ^3.10.2 + version: 3.10.2(@docusaurus/faster@3.10.2(@docusaurus/types@3.10.2(@swc/core@1.16.1)(postcss@8.5.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(postcss@8.5.26))(@mdx-js/react@3.1.1(@types/react@18.3.31)(react@18.3.1))(@rspack/core@1.7.12)(@swc/core@1.16.1)(postcss@8.5.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/tsconfig': specifier: ^3.5.0 version: 3.10.2 + '@docusaurus/types': + specifier: ^3.10.2 + version: 3.10.2(@swc/core@1.16.1)(postcss@8.5.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) typescript: specifier: ^5.4.0 version: 5.9.3 @@ -10452,6 +10461,36 @@ snapshots: - uglify-js - webpack-cli + '@docusaurus/types@3.10.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@mdx-js/mdx': 3.1.1 + '@types/history': 4.7.11 + '@types/mdast': 4.0.4 + '@types/react': 18.3.31 + commander: 5.1.0 + joi: 17.13.4 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' + utility-types: 3.11.0 + webpack: 5.108.4 + webpack-merge: 5.10.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - supports-color + - uglify-js + - webpack-cli + '@docusaurus/utils-common@3.10.2(@swc/core@1.16.1)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(html-minifier-terser@7.2.0)(postcss@8.5.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/types': 3.10.2(@swc/core@1.16.1)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(html-minifier-terser@7.2.0)(postcss@8.5.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11315,6 +11354,16 @@ snapshots: react-fast-compare: 3.2.2 shallowequal: 1.1.0 + '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + invariant: 2.2.4 + prop-types: 15.8.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + '@slorber/remark-comment@1.0.0': dependencies: micromark-factory-space: 1.1.0 @@ -15209,6 +15258,14 @@ snapshots: optionalDependencies: '@swc/core': 1.16.1 + minimizer-webpack-plugin@5.6.1(webpack@5.108.4): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.49.0 + webpack: 5.108.4 + mrmime@2.0.1: {} ms@2.0.0: {} @@ -17415,6 +17472,44 @@ snapshots: webpack-sources@3.5.1: {} + webpack@5.108.4: + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + acorn-import-phases: 1.0.4(acorn@8.17.0) + browserslist: 4.28.6 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.2 + es-module-lexer: 2.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.6.1(webpack@5.108.4) + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + watchpack: 2.5.2 + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + webpack@5.108.4(@swc/core@1.16.1): dependencies: '@types/estree': 1.0.9 diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 76decaa..a799dfe 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -96,7 +96,18 @@ function buildIncludeLoaderRule(args: { }; } -export default async function gitlabPlugin(context: unknown, options: PluginOptions) { +// `context`/`rawOptions` are `unknown` rather than Docusaurus's `LoadContext` / +// our own `PluginOptions` so that this function stays assignable to +// `PluginModule`, whose signature is `(context: LoadContext, options: unknown)`. +// Parameters are checked contravariantly: a narrower `options: PluginOptions` +// does NOT satisfy `options: unknown`, which is why Docusaurus's own plugins +// (declared `options: PluginOptions`) cannot be passed as functions to a typed +// config. Keeping both wide is what makes the documented +// `plugins: [[gitlabPlugin, opts]]` form type-check. Guarded by +// `src/plugin/types.test.ts`; the narrowing back to PluginOptions happens on the +// first line, where `resolveOptions` validates the shape with Joi anyway. +export default async function gitlabPlugin(context: unknown, rawOptions: unknown) { + const options = (rawOptions ?? {}) as PluginOptions; const mode = process.env.NODE_ENV === "production" ? "production" : "development"; const resolved = resolveOptions(options, mode); const loadContext = context as PluginContextLike | undefined; @@ -166,7 +177,10 @@ export default async function gitlabPlugin(context: unknown, options: PluginOpti // instead of concatenating — `append` makes it plain-concat so other // plugins' rule objects pass through unchanged rather than being // merged with ours. - mergeStrategy: { "module.rules": "append" }, + // `as const` is load-bearing: without it the literal widens to `string`, + // which is not a webpack-merge `CustomizeRuleString`, and the whole + // return value stops being a valid `ConfigureWebpackResult`. + mergeStrategy: { "module.rules": "append" as const }, }; }, }; diff --git a/src/plugin/types.test.ts b/src/plugin/types.test.ts new file mode 100644 index 0000000..2384d90 --- /dev/null +++ b/src/plugin/types.test.ts @@ -0,0 +1,50 @@ +import type { LoadContext, Plugin, PluginConfig, PluginModule } from "@docusaurus/types"; +import { describe, it, expect } from "vitest"; +import gitlabPlugin from "./index.js"; + +// Compile-time conformance guard. `@docusaurus/types` is a devDependency only, +// and `tsconfig.build.json` excludes `*.test.ts`, so this import is checked by +// `pnpm run typecheck` without ever reaching `dist/` — a `.d.ts` that imported +// it would fail to resolve for pnpm consumers, who do not get @docusaurus/types +// hoisted into their node_modules (examples/gitlab is the proof). +// +// The assertions below are the contract Docusaurus actually applies to a plugin +// passed as a FUNCTION — the form README.md and examples/gitlab document: +// +// plugins: [[gitlabPlugin, gitlabOptions]] +// +// If any of them stops compiling, that usage breaks for every TypeScript site. + +// The whole module must be a PluginModule: `(context: LoadContext, options: unknown)`. +// Note `options` is `unknown`, not our PluginOptions — a narrower parameter type +// fails contravariantly. Docusaurus's own plugins (e.g. @docusaurus/plugin-sitemap) +// declare `options: PluginOptions` and are NOT assignable here; we deliberately +// hold a stricter line so the documented function form type-checks. +const _module: PluginModule = gitlabPlugin; + +// The returned object must be a valid Plugin. Without this, the return shape is +// inferred and never compared to anything — which is how `mergeStrategy` came to +// widen to `string` and silently violate ConfigureWebpackResult. +async function _returnsPlugin(context: LoadContext): Promise { + return await gitlabPlugin(context, {}); +} + +// And the documented registration form must type-check uncast. +const _config: PluginConfig[] = [[gitlabPlugin, { host: "https://gitlab.com" }]]; + +void _module; +void _returnsPlugin; +void _config; + +describe("gitlabPlugin Docusaurus contract", () => { + it("declares mergeStrategy with a literal CustomizeRuleString, not a widened string", async () => { + // The runtime half of the guard above: `as const` on the "append" literal is + // what keeps configureWebpack's result assignable to ConfigureWebpackResult. + const plugin = await gitlabPlugin({ siteDir: "/site" } as never, { + host: "https://gitlab.example.com", + cache: false, + } as never); + const wp = plugin.configureWebpack!({} as never, false, {} as never, undefined); + expect(wp).toMatchObject({ mergeStrategy: { "module.rules": "append" } }); + }); +}); From 3664021e70441dd36cdc1a92d130c8d1c1bb818e Mon Sep 17 00:00:00 2001 From: Thomas Decaux Date: Tue, 1 Sep 2026 16:02:34 +0200 Subject: [PATCH 4/4] refactor: address review feedback on the PluginModule conformance guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three reviewers (impact, security, code style) converged on the same set of issues. All verified before and after. Test pollution (security + style, PR-introduced): types.test.ts called the factory without an assetDir, so `resolveOptions` defaulted it to the relative "static/gitlab-assets" and the eager `ctx.assets.sync()` created that directory in the repo root on every `pnpm test`. .gitignore:23 only covers examples/*/static/. Confirmed by bisection: the full suite with this file removed creates nothing; the file alone creates it. Both sibling tests avoid this deliberately and say so (index.test.ts:15, packaging.test.ts). The runtime `it()` could not catch the bug it claimed to. Types are erased, so removing `as const` left the assertion passing, and it duplicated index.test.ts:69, which asserts the same thing with the webpack-merge rationale. Dropped it. vitest.config.ts sets passWithNoTests, so a pure type-assertion file is a clean pass — which removes the assetDir problem entirely rather than patching it. Switched to `satisfies`, the idiom this repo already uses (8× in gitlab/fetchers.ts); `void _x` / `const _x` placeholders had no precedent anywhere in src/ or test/, and eslint's varsIgnorePattern "^_" made the `void` statements redundant regardless. The separate `_returnsPlugin` assertion was subsumed by the module-level one, which already pins the return type. Verified the two remaining lines still catch both regressions independently: reverting `as const` and re-narrowing the options parameter each produce 2 errors. Renamed `rawOptions` back to `options` with a `pluginOptions` local, matching the `context`/`loadContext` pattern one line below. The parameter name is public API surface — it shows in dist/plugin/index.d.ts and in consumers' editor tooltips. Wired the consumer-side guard into CI and mise. It was documentation before: ci.yml ran only the root typecheck, whose tsconfig includes just src and test, and mise.toml had no task. The new CI step goes in the `test` job, which already builds dist/ — the example's workspace link resolves to it, so the `lint` job could not run this. Trimmed the contravariance explanation, which had been written three times in near-identical words; CLAUDE.md keeps the canonical copy. Documented instead that `?? {}` is load-bearing: without it `resolveOptions(undefined)` passes Joi's object schema and dies on `opts.host` with a raw TypeError rather than the branded "host is required". Gate: build, typecheck, example typecheck, lint (0 errors), 677 tests across 59 files, no stray static/, and dist/ still free of @docusaurus/types. --- .github/workflows/ci.yml | 2 ++ CLAUDE.md | 6 ++-- mise.toml | 5 ++++ src/plugin/index.ts | 26 ++++++++--------- src/plugin/types.test.ts | 60 +++++++++++----------------------------- 5 files changed, 39 insertions(+), 60 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfd3a7f..a23ab60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,8 @@ jobs: run: pnpm install --frozen-lockfile - name: Build package (needed by the example site workspace link) run: pnpm run build + - name: Type-check the showcase site as a consumer would + run: pnpm --filter example-gitlab run typecheck - name: Run tests (unit + e2e docusaurus build) run: pnpm test diff --git a/CLAUDE.md b/CLAUDE.md index c47b708..c4371b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,8 +65,10 @@ static HTML. The browser never holds a token or calls the GitLab API. `mise run setup | lint | lint:fix | typecheck | test | build`, plus `mise run release` (runs the full gate locally and shows the pending release-please PR — never publishes) and example-site tasks that rebuild - `dist/` first: `gitlab:build`/`gitlab:start` (showcase site, live gitlab.com - data) and `site:build`/`site:start` (`examples/site` is the e2e fixture — + `dist/` first: `gitlab:build`/`gitlab:start`/`gitlab:typecheck` (showcase site, + live gitlab.com data; `gitlab:typecheck` checks the site against the built + `dist/` the way a consumer would, and also runs in CI) + and `site:build`/`site:start` (`examples/site` is the e2e fixture — its stub projects 404 against real gitlab.com, so `site:build` only works with `GITLAB_HOST` pointing at a stub; `site:start` renders Fallbacks in dev). diff --git a/mise.toml b/mise.toml index 655a597..ad86690 100644 --- a/mise.toml +++ b/mise.toml @@ -47,6 +47,11 @@ description = "Build the full GitLab showcase site (examples/gitlab)" depends = ["build"] run = "pnpm --filter example-gitlab run build" +[tasks."gitlab:typecheck"] +description = "Type-check the GitLab showcase site against the built dist/, as a consumer would (examples/gitlab)" +depends = ["build"] +run = "pnpm --filter example-gitlab run typecheck" + [tasks."gitlab:start"] description = "Run the GitLab showcase site dev server (examples/gitlab)" depends = ["build"] diff --git a/src/plugin/index.ts b/src/plugin/index.ts index a799dfe..e85fb75 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -96,20 +96,18 @@ function buildIncludeLoaderRule(args: { }; } -// `context`/`rawOptions` are `unknown` rather than Docusaurus's `LoadContext` / -// our own `PluginOptions` so that this function stays assignable to -// `PluginModule`, whose signature is `(context: LoadContext, options: unknown)`. -// Parameters are checked contravariantly: a narrower `options: PluginOptions` -// does NOT satisfy `options: unknown`, which is why Docusaurus's own plugins -// (declared `options: PluginOptions`) cannot be passed as functions to a typed -// config. Keeping both wide is what makes the documented -// `plugins: [[gitlabPlugin, opts]]` form type-check. Guarded by -// `src/plugin/types.test.ts`; the narrowing back to PluginOptions happens on the -// first line, where `resolveOptions` validates the shape with Joi anyway. -export default async function gitlabPlugin(context: unknown, rawOptions: unknown) { - const options = (rawOptions ?? {}) as PluginOptions; +// Both parameters stay `unknown` so this stays assignable to Docusaurus's +// `PluginModule` — `(context: LoadContext, options: unknown)`. Parameters are +// checked contravariantly, so a narrower `options: PluginOptions` breaks the +// documented `plugins: [[gitlabPlugin, opts]]` form (it is why Docusaurus's own +// plugins cannot be registered that way). Guarded by ./types.test.ts. +// `?? {}` is load-bearing: without it `resolveOptions(undefined)` sails past +// Joi's object schema and dies on `opts.host` with a raw TypeError instead of +// the branded `invalid options — "host" is required`. +export default async function gitlabPlugin(context: unknown, options: unknown) { + const pluginOptions = (options ?? {}) as PluginOptions; const mode = process.env.NODE_ENV === "production" ? "production" : "development"; - const resolved = resolveOptions(options, mode); + const resolved = resolveOptions(pluginOptions, mode); const loadContext = context as PluginContextLike | undefined; const providedSiteDir = loadContext?.siteDir; const siteDir = providedSiteDir ?? process.cwd(); @@ -126,7 +124,7 @@ export default async function gitlabPlugin(context: unknown, rawOptions: unknown // driven separately by the serializable `resolved.fixAutolinks` boolean, so it // never depends on this registry.) const processorsId = `gitlab-out-${processorSeq++}`; - registerOutProcessors(processorsId, options.outProcessors ?? []); + registerOutProcessors(processorsId, pluginOptions.outProcessors ?? []); const ctx = buildContext(resolved); diff --git a/src/plugin/types.test.ts b/src/plugin/types.test.ts index 2384d90..1cccaa3 100644 --- a/src/plugin/types.test.ts +++ b/src/plugin/types.test.ts @@ -1,50 +1,22 @@ -import type { LoadContext, Plugin, PluginConfig, PluginModule } from "@docusaurus/types"; -import { describe, it, expect } from "vitest"; +import type { PluginConfig, PluginModule } from "@docusaurus/types"; import gitlabPlugin from "./index.js"; -// Compile-time conformance guard. `@docusaurus/types` is a devDependency only, -// and `tsconfig.build.json` excludes `*.test.ts`, so this import is checked by -// `pnpm run typecheck` without ever reaching `dist/` — a `.d.ts` that imported -// it would fail to resolve for pnpm consumers, who do not get @docusaurus/types -// hoisted into their node_modules (examples/gitlab is the proof). -// -// The assertions below are the contract Docusaurus actually applies to a plugin -// passed as a FUNCTION — the form README.md and examples/gitlab document: +// Compile-time conformance guard for the documented registration form, // // plugins: [[gitlabPlugin, gitlabOptions]] // -// If any of them stops compiling, that usage breaks for every TypeScript site. - -// The whole module must be a PluginModule: `(context: LoadContext, options: unknown)`. -// Note `options` is `unknown`, not our PluginOptions — a narrower parameter type -// fails contravariantly. Docusaurus's own plugins (e.g. @docusaurus/plugin-sitemap) -// declare `options: PluginOptions` and are NOT assignable here; we deliberately -// hold a stricter line so the documented function form type-checks. -const _module: PluginModule = gitlabPlugin; - -// The returned object must be a valid Plugin. Without this, the return shape is -// inferred and never compared to anything — which is how `mergeStrategy` came to -// widen to `string` and silently violate ConfigureWebpackResult. -async function _returnsPlugin(context: LoadContext): Promise { - return await gitlabPlugin(context, {}); -} - -// And the documented registration form must type-check uncast. -const _config: PluginConfig[] = [[gitlabPlugin, { host: "https://gitlab.com" }]]; - -void _module; -void _returnsPlugin; -void _config; +// which Docusaurus type-checks against `PluginConfig` (see README.md and +// examples/gitlab). `PluginModule` pins the RETURN type too, so a widened +// `mergeStrategy` — the `as const` in ./index.js — fails here as well. +// +// `@docusaurus/types` is a devDependency and `tsconfig.build.json` excludes +// `*.test.ts`, so this import is checked by `pnpm run typecheck` without ever +// reaching `dist/`. That matters: a `.d.ts` importing it would not resolve for +// pnpm consumers, who do not get the package hoisted — examples/gitlab has to +// declare it explicitly. +// +// No runtime assertions: `passWithNoTests` (vitest.config.ts) makes that a pass, +// and ./index.test.ts already covers the runtime behaviour. -describe("gitlabPlugin Docusaurus contract", () => { - it("declares mergeStrategy with a literal CustomizeRuleString, not a widened string", async () => { - // The runtime half of the guard above: `as const` on the "append" literal is - // what keeps configureWebpack's result assignable to ConfigureWebpackResult. - const plugin = await gitlabPlugin({ siteDir: "/site" } as never, { - host: "https://gitlab.example.com", - cache: false, - } as never); - const wp = plugin.configureWebpack!({} as never, false, {} as never, undefined); - expect(wp).toMatchObject({ mergeStrategy: { "module.rules": "append" } }); - }); -}); +gitlabPlugin satisfies PluginModule; +[[gitlabPlugin, { host: "https://gitlab.com" }]] satisfies PluginConfig[];