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 45e6deb..c4371b1 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 @@ -54,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/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/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/package.json b/package.json index 2f74745..66e7761 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": { @@ -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..e85fb75 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -96,9 +96,18 @@ function buildIncludeLoaderRule(args: { }; } -export default async function gitlabPlugin(context: unknown, options: 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(); @@ -115,7 +124,7 @@ export default async function gitlabPlugin(context: unknown, options: PluginOpti // 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); @@ -166,7 +175,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..1cccaa3 --- /dev/null +++ b/src/plugin/types.test.ts @@ -0,0 +1,22 @@ +import type { PluginConfig, PluginModule } from "@docusaurus/types"; +import gitlabPlugin from "./index.js"; + +// Compile-time conformance guard for the documented registration form, +// +// plugins: [[gitlabPlugin, gitlabOptions]] +// +// 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. + +gitlabPlugin satisfies PluginModule; +[[gitlabPlugin, { host: "https://gitlab.com" }]] satisfies PluginConfig[];