diff --git a/.github/renovate.json b/.github/renovate.json index fb61f2125..1c628606c 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -17,6 +17,13 @@ "labels": ["security", "dependencies"] }, "packageRules": [ + { + "description": "Direct Lexical imports must share MDXEditor's installed modules (docs/specs/dor-tools-builtin.md -> Markdown editor). Update these exact pins in the MDXEditor upgrade PR when its Lexical dependencies change; independent bumps can install a second copy even when grouped together.", + "matchManagers": ["npm"], + "matchFileNames": ["dor-tools-builtin/package.json"], + "matchPackageNames": ["lexical", "@lexical/**"], + "enabled": false + }, { "description": "The built-in Markdown editor stays on Mermaid 11; see docs/specs/dor-tools-builtin.rationale.md, Markdown editor", "matchPackageNames": ["mermaid"], diff --git a/docs/specs/dor-tools-builtin.md b/docs/specs/dor-tools-builtin.md index 62525a1e6..39d4b8b4e 100644 --- a/docs/specs/dor-tools-builtin.md +++ b/docs/specs/dor-tools-builtin.md @@ -43,6 +43,7 @@ Source of truth: `saveText` / `StorePath` in `dor-tools-builtin/src/editor-route `.md` and `.markdown` open in the bundled MDXEditor page; `.mdx` and other text stay in Monaco. Saving, reloading, and dirty reports follow [Editing files](#editing-files). +- **Must resolve the viewer's direct `lexical` and `@lexical/link` imports to the same installed modules MDXEditor uses** (rationale). - **Must support GFM tables, task lists, frontmatter, code blocks, fenced `mermaid` diagrams, and a whole-document source mode**, which opens with the error when the rich editor cannot parse a file. Mermaid stays on 11 (rationale). - **Must keep HTML comments verbatim and save with one final newline, the file's majority bullet and thematic-break marker, and unpadded tables**; the rest follows MDXEditor's serialization. Its normalization of loaded text is not an edit (rationale). - **Never let document HTML reach the live DOM outside an allowlist** of tags and attributes, `href` limited to `http(s):`, `mailto:`, fragments, and scheme-less paths; saving keeps every original attribute, escaped, and HTML images serialize in an inert document. Mermaid diagrams are the one other path, under mermaid's own sanitizer (`docs/specs/security-local.md` → "Local-file viewer"; rationale). diff --git a/docs/specs/dor-tools-builtin.rationale.md b/docs/specs/dor-tools-builtin.rationale.md index c9f98c46a..00eb8b878 100644 --- a/docs/specs/dor-tools-builtin.rationale.md +++ b/docs/specs/dor-tools-builtin.rationale.md @@ -18,6 +18,8 @@ Without a title, a viewer's header falls back to its running command, `dor __vie ## Markdown editor +MDXEditor 4.3.3 depends on Lexical `^0.48.0`; independent direct bumps to 0.52 install a second copy (PRs #1195 and #1205, 2026-10). Lexical's active editor state and node classes belong to its module instance, and the viewer's relative-link patch changes `LinkNode.prototype`, so even a successful build can leave editor helpers using the wrong state or patching a class MDXEditor never instantiates. Renovate leaves these direct pins to the MDXEditor upgrade PR; grouping alone does not select compatible versions. + MDXEditor 4.3 parses `` and discards it, and trims and re-serializes the whole document on export; without the comment node and style detection, saving a typical README after a one-word edit deleted its comments and rewrote every `-` bullet as `*` (measured 2026-10). Image sizes live in the document because GitHub shows an `` without a `width` pixel for pixel: a Retina screenshot shown here at its recorded scale but left unsized in the file came out twice as large on GitHub. Writing the width on paste keeps the two renderings alike (2026-10). diff --git a/dor-tools-builtin/test/markdown-dependencies.test.mjs b/dor-tools-builtin/test/markdown-dependencies.test.mjs new file mode 100644 index 000000000..d1c1acb64 --- /dev/null +++ b/dor-tools-builtin/test/markdown-dependencies.test.mjs @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { test } from 'node:test'; + +const viewer = createRequire(new URL('../viewer/markdown.tsx', import.meta.url)); +const editor = createRequire(viewer.resolve('@mdxeditor/editor')); + +// Compare installed module identities, not just declared ranges or versions: +// the editor state, node classes, and LinkNode prototype patch must be shared. +// When MDXEditor moves to another Lexical release, update our direct pins in +// that same PR. This runs in the package's normal test suite and root CI. +for (const name of ['lexical', '@lexical/link']) { + test(`the Markdown viewer and MDXEditor share ${name}`, () => { + assert.equal(viewer.resolve(name), editor.resolve(name), + `Update the direct ${name} pin with @mdxeditor/editor; separate copies break the Markdown editor`); + }); +} + +test('the viewer LinkNode uses the editor’s Lexical state', () => { + const link = createRequire(viewer.resolve('@lexical/link')); + assert.equal(link.resolve('lexical'), editor.resolve('lexical')); +}); diff --git a/scripts/spec-lint-selftest.mjs b/scripts/spec-lint-selftest.mjs index e0c990ee4..95b7c164c 100644 --- a/scripts/spec-lint-selftest.mjs +++ b/scripts/spec-lint-selftest.mjs @@ -74,8 +74,10 @@ const CASES = [ ['check 8: an external rationale key with no heading', EXTERNAL_RATIONALE, '\n## No matching heading\n'], ['check 13: a missing heading in an external spec', SOURCE, `\n// ${EXTERNAL_SPEC} -> "No Such Heading"\n`], ['check 4: a repo path that does not exist', SPEC, '\nSee `lib/src/no-such-file.ts`.\n'], - ['check 11: a (rationale) marker under a heading the rationale does not key', SPEC, '\n## Planted\n\nA rule (rationale).\n'], - ['check 11: the marker as the last item of its parenthetical', SPEC, '\n## Planted\n\nA rule (see below; rationale).\n'], + // Start a new root: a rationale keyed to the original H1 also covers every + // appended H2, so that would not plant a defect in specs such as security.md. + ['check 11: a (rationale) marker under a heading the rationale does not key', SPEC, '\n# Planted\n\nA rule (rationale).\n'], + ['check 11: the marker as the last item of its parenthetical', SPEC, '\n# Planted\n\nA rule (see below; rationale).\n'], ['check 12: a bare file name in Source of truth', SPEC, '\nSource of truth: `lint-kit.mjs`.\n'], ['check 12: a bare file name under a punctuated lead-in', SPEC, '\nSource of truth, all in `lib/src/lib/`: `Wall.tsx`.\n'], ['check 12: a symbol the named file lacks', SPEC, '\nSource of truth: `noSuchSymbolXyz` in `scripts/lint-kit.mjs`.\n'],