Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ jobs:
- run: pnpm test
- run: pnpm dev:prepare
- run: pnpm typecheck
# Builds the playground, which exercises the layer end to end: content
# pages, landing, OG images, and the generated ISR route rules.
# Builds the playground: module setup, bundling, and the generated ISR route rules.
- run: pnpm build
- run: pnpm test:e2e
env:
GITHUB_TOKEN: ${{ github.token }}
NUXT_DOCS_GITHUB_BRANCH: ${{ github.sha }}
# Continuous preview releases via pkg.pr.new: every commit gets an installable
# tarball (https://pkg.pr.new/comarkdown/comark-docs@<sha>), so downstream sites
# can test the layer before a release. Requires the pkg.pr.new GitHub App.
Comment thread
larbish marked this conversation as resolved.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@
"typecheck": "nuxt typecheck playground",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run",
"test:watch": "vitest",
"test": "vitest run --project unit",
"test:e2e": "vitest run --project e2e",
"test:watch": "vitest --project unit",
"dev:prepare": "nuxt prepare playground"
},
"dependencies": {
Expand Down Expand Up @@ -95,6 +96,7 @@
"@opentelemetry/resources": "^2.11.0",
"@opentelemetry/sdk-trace-base": "^2.11.0",
"@opentelemetry/sdk-trace-node": "^2.11.0",
"es-module-lexer": "^3.0.2",
"eslint": "^10.10.0",
"nuxt": "^4.6.0",
"typescript": "^6.0.3",
Expand Down
133 changes: 133 additions & 0 deletions patches/nitropack@2.13.4.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
nitropack@2.13.4, two fixes until a Nitro v2 release ships them:

1. dist/presets/vercel/utils.mjs: nitrojs/nitro#4651, header-only Vercel routes need `continue: true`.
2. dist/rollup/index.mjs: externals map files back to pattern exports (`comark/plugins/*`).
Without it, layer components importing a comark plugin crash SSR on Nuxt 4.6.
Not upstream yet.

Each patched block is tagged with a `[comark-docs patch]` comment.

diff --git a/dist/presets/vercel/utils.mjs b/dist/presets/vercel/utils.mjs
index 6b4a25e30eceded9b820a446c3c4519f4f43c0ac..8a807ea3d67b754e2bc409914105cf97fb7ff5d9 100644
--- a/dist/presets/vercel/utils.mjs
+++ b/dist/presets/vercel/utils.mjs
@@ -129,19 +129,24 @@ function generateBuildConfig(nitro, o11Routes) {
)
},
routes: [
- // Redirect and header rules
- ...rules.filter(([_, routeRules]) => routeRules.redirect || routeRules.headers).map(([path, routeRules]) => {
+ // [comark-docs patch] nitrojs/nitro#4651, merged in Nitro v2 but not released yet.
+ // Header-only rules (least specific first, so more specific headers override on `continue`)
+ ...rules.filter(([_, routeRules]) => routeRules.headers && !routeRules.redirect).reverse().map(([path, routeRules]) => ({
+ src: path.replace("/**", "/(.*)"),
+ headers: routeRules.headers,
+ continue: true
+ })),
+ // Redirect rules
+ ...rules.filter(
+ (entry) => !!entry[1].redirect
+ ).map(([path, routeRules]) => {
let route = {
- src: path.replace("/**", "/(.*)")
+ src: path.replace("/**", "/(.*)"),
+ status: routeRules.redirect.statusCode,
+ headers: {
+ Location: routeRules.redirect.to.replace("/**", "/$1")
+ }
};
- if (routeRules.redirect) {
- route = defu(route, {
- status: routeRules.redirect.statusCode,
- headers: {
- Location: routeRules.redirect.to.replace("/**", "/$1")
- }
- });
- }
if (routeRules.headers) {
route = defu(route, { headers: routeRules.headers });
}
diff --git a/dist/rollup/index.mjs b/dist/rollup/index.mjs
index 9aadcbc7925e965062670f1d86436f8e0830b200..3d55aefa5647fe6f313aa86abf3ae2e34c3aebab 100644
--- a/dist/rollup/index.mjs
+++ b/dist/rollup/index.mjs
@@ -413,10 +413,19 @@ function externals$1(opts) {
}
const packageEntry = tryResolve(pkgName, importer);
if (packageEntry !== id) {
- const guessedSubpath = await lookupNodeModuleSubpath(id).catch(() => null);
- const resolvedGuess = guessedSubpath && tryResolve(join(pkgName, guessedSubpath), importer);
- if (resolvedGuess === id) {
- trackedExternals.add(resolvedGuess);
+ // [comark-docs patch] Map files back to pattern exports (`comark/plugins/*`), not upstream yet.
+ const resolvesToId = (subpath) => !!subpath && tryResolve(join(pkgName, subpath), importer) === id;
+ const exactSubpath = await lookupNodeModuleSubpath(id).catch(
+ () => null
+ );
+ const guessedSubpath = resolvesToId(exactSubpath) ? exactSubpath : (
+ // `lookupNodeModuleSubpath` only matches exact export targets
+ (await lookupPatternExportSubpaths(id)).find(
+ (subpath) => resolvesToId(subpath)
+ )
+ );
+ if (guessedSubpath) {
+ trackedExternals.add(id);
return {
id: join(pkgName, guessedSubpath),
external: true
@@ -686,6 +695,55 @@ function applyProductionCondition(exports$1) {
applyProductionCondition(exports$1[key]);
}
}
+// [comark-docs patch] Pattern-export lookup used by the externals plugin above.
+async function lookupPatternExportSubpaths(id) {
+ const { dir, name, subpath } = parseNodeModulePath$1(id);
+ if (!dir || !name || !subpath) {
+ return [];
+ }
+ const { exports } = await readPackageJSON(join(dir, name)).catch(
+ () => ({})
+ );
+ return findPatternExportSubpaths(exports, subpath);
+}
+function findPatternExportSubpaths(exports, subpath) {
+ if (!exports || typeof exports !== "object" || Array.isArray(exports)) {
+ return [];
+ }
+ const subpaths = [];
+ for (const [key, value] of Object.entries(exports)) {
+ if (!key.startsWith("./") || key.split("*").length !== 2) {
+ continue;
+ }
+ for (const target of exportTargets(value)) {
+ const parts = target.split("*");
+ if (parts.length !== 2) {
+ continue;
+ }
+ const [prefix, suffix] = parts;
+ if (subpath.length > prefix.length + suffix.length && subpath.startsWith(prefix) && subpath.endsWith(suffix)) {
+ const match = subpath.slice(
+ prefix.length,
+ subpath.length - suffix.length
+ );
+ subpaths.push(key.replace("*", match));
+ }
+ }
+ }
+ return subpaths;
+}
+function exportTargets(value) {
+ if (typeof value === "string") {
+ return [value];
+ }
+ if (Array.isArray(value)) {
+ return value.flatMap((item) => exportTargets(item));
+ }
+ if (value && typeof value === "object") {
+ return Object.values(value).flatMap((item) => exportTargets(item));
+ }
+ return [];
+}
async function isFile$1(file) {
try {
const stat = await promises.stat(file);
27 changes: 17 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ minimumReleaseAgeExclude:
overrides:
# Keep the workspace on a single h3 major (v1), matching comark-content.
h3: ^1.15.11
# Until nitrojs/nitro#4651 is released: header-only Vercel routes need `continue: true`.
nitropack: https://pkg.pr.new/nitrojs/nitro/nitropack@4651

patchedDependencies:
# Until released in Nitro v2, two fixes on top of 2.13.4:
# - nitrojs/nitro#4651: header-only Vercel routes need `continue: true`.
# - Externals map files back to pattern exports, or layer components importing `comark/plugins/*` crash SSR.
nitropack@2.13.4: patches/nitropack@2.13.4.patch
50 changes: 50 additions & 0 deletions test/e2e/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { readdirSync, readFileSync } from 'node:fs'
import { isBuiltin } from 'node:module'
import { join, relative } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { init, parse } from 'es-module-lexer'
import { resolveModuleURL } from 'exsolve'
import { expect, it } from 'vitest'

const serverDir = fileURLToPath(new URL('../../playground/.output/server/', import.meta.url))

// Optional peers imported behind a try/catch, absent unless a site installs them.
const OPTIONAL_IMPORTS = new Set([
// `@nuxtjs/mcp-toolkit` Code Mode.
'secure-exec',
])

/** The bundled chunks: the traced `node_modules/` are third-party files Nitro copied as they are. */
function bundledFiles(): string[] {
return readdirSync(serverDir, { withFileTypes: true, recursive: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.mjs'))
.map((entry) => join(entry.parentPath, entry.name))
.filter((file) => !relative(serverDir, file).startsWith('node_modules'))
}

// A lazy chunk (a component, a route) only fails once requested, so the route crawl alone can miss it.
it('every import in the server bundle resolves inside the build output', async () => {
await init()
const files = bundledFiles()
const broken: string[] = []
let checked = 0

for (const file of files) {
const [imports] = parse(readFileSync(file, 'utf8'))
for (const entry of imports) {
// `import.meta`, computed dynamic imports and template globs name no single module.
const specifier = entry.type === 'dynamic' && entry.glob ? undefined : entry.specifier
if (!specifier || isBuiltin(specifier) || OPTIONAL_IMPORTS.has(specifier)) continue
checked++
const resolved = resolveModuleURL(specifier, { from: pathToFileURL(file), try: true })
// Resolving into the repo's own `node_modules` works locally but not once deployed.
if (!resolved?.startsWith(pathToFileURL(serverDir).href)) {
broken.push(`${relative(serverDir, file)} imports ${specifier}`)
}
}
}

// Guards against a vacuous pass, e.g. the lexer reporting no specifiers after an API change.
expect(checked).toBeGreaterThan(files.length)
expect(broken).toEqual([])
})
Loading
Loading