Skip to content

render-helper: make the node build loadable by plain Node - #1566

Merged
feruzm merged 4 commits into
developfrom
fix/render-helper-node-esm
Aug 20, 2026
Merged

render-helper: make the node build loadable by plain Node#1566
feruzm merged 4 commits into
developfrom
fix/render-helper-node-esm

Conversation

@feruzm

@feruzm feruzm commented Aug 20, 2026

Copy link
Copy Markdown
Member

Fixes #1519.

remarkable/linkify is a legacy module directory: a folder holding nothing but a package.json stub with main/module, no index.js. CJS resolves that stub, Node's ESM resolver refuses it, so the emitted dist/node/index.mjs could not be loaded by plain Node at all:

Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '.../node_modules/remarkable/linkify' is not
supported resolving ES modules imported from .../@ecency/render-helper/dist/node/index.mjs

Reproduced on Node 24 and Node 18, the build's own target. Every bundler resolves the directory happily, which is why nothing here caught it: the failure only exists where there is no bundler. The newsletter service hit it in production and has been carrying a createRequire workaround since.

The change

One option, on the node build only:

noExternal: ["remarkable"],

The shared external list is untouched, so the browser build is byte-identical. That matters more than it sounds: React Native resolves the react-native export condition to dist/browser/index.js, so the file the mobile app loads is unchanged by this PR, today and after any future version bump.

Why not the obvious one-line import change

Pointing the source at a file inside remarkable looks cheaper and is not:

  • remarkable/dist/esm/linkify.js is a .js in a package with no "type": "module", so it is nominally CommonJS. It only loads on Node >= 22.7 via unflagged module-syntax detection, and fails outright on Node 18 and 20. The package declares no engines and is published publicly.
  • remarkable/dist/cjs/linkify.js does work everywhere, but it drags autolinker's CommonJS build into every browser bundle instead of its tree-shakeable one, about +5.4 KB gzipped on ecency.com and on every self-hosted blog.

Inlining costs ~237 KB in a build that only servers and CLIs load, and nothing anywhere else.

The test

test/dist-loads-in-plain-node.test.ts spawns the real node binary against the real build output, for both the import and require conditions, and asserts the module loads, renders, and autolinks. Importing from inside vitest would prove nothing, since Vite is exactly the layer that hid this.

It runs in CI: PR build does pnpm build:packages and then pnpm -r test, in that order.

Negative-controlled. With noExternal removed and the package rebuilt:

× the import condition loads as ESM and renders
× keeps remarkable inlined in the node builds
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '.../remarkable/linkify' ...

Verification

Three Fable agents were pointed at this with instructions to break it, not to bless it.

Mobile. dist/browser/index.js is byte-identical by sha256 (8eb87e6c…) across the committed file, two rebuilds, and the npm-published 2.5.26. The real metro-resolver 0.82.5, driven with vision-mobile's actual config, resolves @ecency/render-helper to that file, so the fix contributes zero bytes to what React Native loads. A future version bump delivers exactly what npm latest already delivers. vision-mobile's own suite: 896 tests pass.

Forcing unstable_enablePackageExports: false, the fallback path, resolves to dist/node/index.cjs, which is the file that changed. It still works, and it got safer: the pre-fix fallback pulled remarkable's browser variant, which decodes entities through a DOM <textarea>, while the inlined copy is the DOM-free node variant.

Output. A 74-input corpus across 8 call shapes (renderPostBody both modes, postBodySummary at three lengths, simpleMarkdownToHTML, catchPostImage) is byte-identical before and after, with the autolink comparison confirmed non-vacuous.

Consumers. The newsletter service's plain import now works against a packed tarball of this build, and its existing createRequire path still works, so removing the workaround is optional rather than forced. hosting/api unaffected. apps/web builds (Next 15 production) and its suite passes, with no duplicate remarkable in .next. apps/self-hosted builds, its 1004 tests pass, and the node-globals guard is clean.

They also found three real holes in the test, which are fixed in this branch:

  • it asserted against the committed dist, which is deliberately stale here, so it went red on a fresh checkout and would have gone red on web-build.yml, which runs pnpm -r test with no preceding pnpm build:packages. It builds its own output now.
  • the guard regex missed a bare side-effect import "remarkable/linkify", which is the exact shape the issue describes. All six emission forms are covered now, verified one by one, and it still ignores the deprecation string inside the inlined remarkable that reads import linkify from 'remarkable/linkify'.
  • building to /tmp failed with ERR_MODULE_NOT_FOUND on the first external, since Node resolves bare specifiers by walking up from the importing file. It builds inside the package tree.

Local: render-helper 1256 tests, typecheck and lint clean; self-hosted 1004 tests and a clean build; apps/web builds.

Note for the reviewer

packages/render-helper/dist is deliberately left at its committed state. The patch:render-helper label is what makes auto-changeset bump the version, rebuild the package and commit dist back onto this branch. Without that label the source fix reaches nobody, because both apps consume the package through workspace:*, which means through the committed dist.

Nothing needs to change in vision-mobile for this. It is unaffected by the bug and unaffected by the fix.

remarkable/linkify is a legacy module directory: a folder holding nothing but a
package.json stub with main and module, no index.js. CJS resolves that stub,
Node's ESM resolver refuses it, so the emitted dist/node/index.mjs could not be
imported by plain Node at all. Every bundler resolves the directory happily,
which is why nothing here caught it. The newsletter service hit it in production
and has been loading the package through createRequire since.

remarkable is inlined into the node build now, and only that build. The shared
external list is untouched, so dist/browser/index.js comes out byte for byte
what it is today, which matters because that is the file React Native loads
through the react-native export condition.

Not the smaller-looking fix of importing a file inside remarkable:
remarkable/dist/esm/linkify.js is a .js in a package with no type module, so it
is nominally CommonJS and only loads on Node 22.7 and later through
module-syntax detection, while this build targets node18. The .cjs file does
work everywhere but drags autolinker's CommonJS build into every browser bundle
in place of its tree-shakeable one, which is about 5.4KB gzipped on ecency.com
and on every self-hosted blog. Inlining costs 237KB in a build only servers and
CLIs load.

The test spawns the real node binary against real build output, for both node
conditions, and checks the module loads, renders and autolinks. Running it
inside vitest would prove nothing, since Vite is the layer that hid this. It
builds into a scratch directory rather than reading dist, because dist is
committed and the changeset bot is what rebuilds it, so on this branch the
committed copy is legitimately stale and asserting against it would go red on a
fresh checkout and on web-build.yml, which runs the tests with no preceding
package build.
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5eb92a77-b554-4bd0-b23b-bb2f43816c18


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 20, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Make render-helper Node bundles loadable without a bundler

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Inline Remarkable only in Node bundles to avoid unsupported ESM directory imports.
• Preserve browser and React Native bundles by retaining shared externalization.
• Verify ESM, CommonJS, rendering, and autolinking through real Node subprocesses.
Diagram

graph TD
  SRC["Render source"] --> CFG["tsup config"] --> NB["Node build"] --> ESM["Node ESM"] --> NODE["Plain Node"]
  REM["Remarkable"] -->|inlined| NB --> CJS["Node CommonJS"] --> NODE
  CFG --> BB["Browser bundle"]
  REM -->|externalized| BB
Loading
High-Level Assessment

The PR's build-specific inlining is the best approach because it restores Node 18-compatible ESM loading without changing browser output. Deep-importing Remarkable's ESM file depends on newer Node syntax detection, while using its CommonJS file would increase browser bundles; consumer-side createRequire workarounds would leave the published package defective.

Files changed (2) +138 / -0

Bug fix (1) +20 / -0
tsup.config.tsInline Remarkable exclusively in Node bundles +20/-0

Inline Remarkable exclusively in Node bundles

• Adds a Node-only noExternal override for Remarkable so generated ESM no longer contains the unsupported remarkable/linkify directory import. Shared browser externalization remains unchanged, preserving browser and React Native output behavior.

packages/render-helper/tsup.config.ts

Tests (1) +118 / -0
dist-loads-in-plain-node.test.tsAdd plain-Node distribution loading regression tests +118/-0

Add plain-Node distribution loading regression tests

• Builds fresh ESM and CommonJS artifacts into a temporary package-local directory, then loads them through real Node subprocesses. It verifies rendering and autolinking, confirms Remarkable is inlined, and pins package export conditions to the tested outputs.

packages/render-helper/test/dist-loads-in-plain-node.test.ts

Comment thread packages/render-helper/test/dist-loads-in-plain-node.test.ts Fixed
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Test uses wrong filename suffix ⊘ Outdated 📜 Skill insight ⚙ Maintainability
Description
The new non-React test is named dist-loads-in-plain-node.test.ts instead of using the required
.spec.ts pattern.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1]

+import { execFile } from "node:child_process";
Relevance

●●● Strong

Non-React test naming rules are deterministic; no close rejection precedent outweighs the stated
.spec.ts requirement.

PR-#753

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668286 requires non-React utilities and tests to use .spec.ts, while this PR adds a test
file named dist-loads-in-plain-node.test.ts.

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-1]
Skill: add-test: Skill: add-test: Skill: add-test: Skill: add-test: Skill: add-test: Skill: add-test: Skill: add-test: Skill: add-test

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new non-React test uses the prohibited `.test.ts` filename pattern.
## Issue Context
Compliance rule 2668286 requires non-React test files to use the `.spec.ts` suffix. Rename the file while preserving its test contents and ensuring test discovery still includes it.
## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. JSON parses yield any ⊘ Outdated 📘 Rule violation ⚙ Maintainability
Description
The new test assigns unvalidated JSON.parse() results to inferred variables and then accesses
their properties, introducing implicit any values. This removes type checking for both
spawned-process output and the package manifest shape.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[R65-67]

+        const result = JSON.parse(
+            await inNode(["--input-type=module", "-e", `import(${JSON.stringify(join(out, "index.mjs"))}).then((m) => { ${report("m")} })`])
+        );
Relevance

●●● Strong

Recent repository precedent accepted removing any casts in new tests, directly reinforcing the
no-any TypeScript rule.

PR-#1489

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668119 disallows implicit or explicit any in new TypeScript code. The results of
JSON.parse() at lines 65 and 110 retain its any return type and are used through untyped
property access.

Rule 2668119: Disallow implicit and any types in new TypeScript code
packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New `JSON.parse()` calls produce implicit `any` values that are used without validation.
## Issue Context
Define explicit result and package-manifest types, parse into `unknown`, and validate or narrow the values before property access. Apply this to both spawned Node output and the package manifest.
## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. JSON parses yield any ⊘ Outdated 📘 Rule violation ⚙ Maintainability
Description
The new test assigns unvalidated JSON.parse() results to inferred variables and then accesses
their properties, introducing implicit any values. This removes type checking for both
spawned-process output and the package manifest shape.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[R65-67]

+        const result = JSON.parse(
+            await inNode(["--input-type=module", "-e", `import(${JSON.stringify(join(out, "index.mjs"))}).then((m) => { ${report("m")} })`])
+        );
Relevance

●●● Strong

Recent repository precedent accepted removing any casts in new tests, directly reinforcing the
no-any TypeScript rule.

PR-#1489

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668119 disallows implicit or explicit any in new TypeScript code. The results of
JSON.parse() at lines 65 and 110 retain its any return type and are used through untyped
property access.

Rule 2668119: Disallow implicit and any types in new TypeScript code
packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New `JSON.parse()` calls produce implicit `any` values that are used without validation.
## Issue Context
Define explicit result and package-manifest types, parse into `unknown`, and validate or narrow the values before property access. Apply this to both spawned Node output and the package manifest.
## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (4)
4. Test uses wrong filename suffix ⊘ Outdated 📜 Skill insight ⚙ Maintainability
Description
The new non-React test is named dist-loads-in-plain-node.test.ts instead of using the required
.spec.ts pattern.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1]

+import { execFile } from "node:child_process";
Relevance

●●● Strong

Non-React test naming rules are deterministic; no close rejection precedent outweighs the stated
.spec.ts requirement.

PR-#753

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668286 requires non-React utilities and tests to use .spec.ts, while this PR adds a test
file named dist-loads-in-plain-node.test.ts.

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-1]
Skill: add-test: Skill: add-test: Skill: add-test: Skill: add-test

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new non-React test uses the prohibited `.test.ts` filename pattern.
## Issue Context
Compliance rule 2668286 requires non-React test files to use the `.spec.ts` suffix. Rename the file while preserving its test contents and ensuring test discovery still includes it.
## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. JSON parses yield any ⊘ Outdated 📘 Rule violation ⚙ Maintainability
Description
The new test assigns unvalidated JSON.parse() results to inferred variables and then accesses
their properties, introducing implicit any values. This removes type checking for both
spawned-process output and the package manifest shape.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[R65-67]

+        const result = JSON.parse(
+            await inNode(["--input-type=module", "-e", `import(${JSON.stringify(join(out, "index.mjs"))}).then((m) => { ${report("m")} })`])
+        );
Relevance

●●● Strong

Recent repository precedent accepted removing any casts in new tests, directly reinforcing the
no-any TypeScript rule.

PR-#1489

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668119 disallows implicit or explicit any in new TypeScript code. The results of
JSON.parse() at lines 65 and 110 retain its any return type and are used through untyped
property access.

Rule 2668119: Disallow implicit and any types in new TypeScript code
packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New `JSON.parse()` calls produce implicit `any` values that are used without validation.
## Issue Context
Define explicit result and package-manifest types, parse into `unknown`, and validate or narrow the values before property access. Apply this to both spawned Node output and the package manifest.
## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Test uses wrong filename suffix ⊘ Outdated 📜 Skill insight ⚙ Maintainability
Description
The new non-React test is named dist-loads-in-plain-node.test.ts instead of using the required
.spec.ts pattern.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1]

+import { execFile } from "node:child_process";
Relevance

●●● Strong

Non-React test naming rules are deterministic; no close rejection precedent outweighs the stated
.spec.ts requirement.

PR-#753

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668286 requires non-React utilities and tests to use .spec.ts, while this PR adds a test
file named dist-loads-in-plain-node.test.ts.

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-1]
Skill: add-test: Skill: add-test

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new non-React test uses the prohibited `.test.ts` filename pattern.
## Issue Context
Compliance rule 2668286 requires non-React test files to use the `.spec.ts` suffix. Rename the file while preserving its test contents and ensuring test discovery still includes it.
## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Node 18 remains untested ⊘ Outdated 🐞 Bug ☼ Reliability
Description
inNode always launches process.execPath, while every CI workflow runs Vitest on Node 24, so the
new regression test cannot detect load failures specific to the Node 18 build target. This is
particularly risky for a fix whose implementation choice depends on compatibility differences
between Node versions.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[54]

+    const { stdout } = await exec(process.execPath, args, { cwd: PKG, timeout: 60_000 });
Evidence
The new helper invokes process.execPath, so it cannot test a Node version different from Vitest's
runtime. The Node build explicitly targets Node 18, while the PR workflow matrix installs only Node
24.

packages/render-helper/test/dist-loads-in-plain-node.test.ts[52-55]
packages/render-helper/tsup.config.ts[69-72]
.github/workflows/PR-branch.yml[20-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The plain-Node smoke test launches the same Node binary running Vitest. CI only uses Node 24, although the generated package explicitly targets Node 18, leaving version-specific loader regressions uncovered.
## Issue Context
The build uses `target: "node18"`, but `process.execPath` resolves to the CI runner's Node 24 executable. Add a CI path that executes the built artifacts with Node 18, or provide the test an explicit Node 18 executable while retaining the existing Node 24 coverage.
## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[52-55]
- packages/render-helper/tsup.config.ts[69-72]
- .github/workflows/PR-branch.yml[20-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/render-helper/test/dist-loads-in-plain-node.test.ts Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. JSON parses yield any ⊘ Outdated 📘 Rule violation ⚙ Maintainability
Description
The new test assigns unvalidated JSON.parse() results to inferred variables and then accesses
their properties, introducing implicit any values. This removes type checking for both
spawned-process output and the package manifest shape.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[R65-67]

+        const result = JSON.parse(
+            await inNode(["--input-type=module", "-e", `import(${JSON.stringify(join(out, "index.mjs"))}).then((m) => { ${report("m")} })`])
+        );
Relevance

●●● Strong

Recent repository precedent accepted removing any casts in new tests, directly reinforcing the
no-any TypeScript rule.

PR-#1489

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668119 disallows implicit or explicit any in new TypeScript code. The results of
JSON.parse() at lines 65 and 110 retain its any return type and are used through untyped
property access.

Rule 2668119: Disallow implicit and any types in new TypeScript code
packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New `JSON.parse()` calls produce implicit `any` values that are used without validation.

## Issue Context
Define explicit result and package-manifest types, parse into `unknown`, and validate or narrow the values before property access. Apply this to both spawned Node output and the package manifest.

## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[65-67]
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[110-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Test uses wrong filename suffix ⊘ Outdated 📜 Skill insight ⚙ Maintainability
Description
The new non-React test is named dist-loads-in-plain-node.test.ts instead of using the required
.spec.ts pattern.
Code

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1]

+import { execFile } from "node:child_process";
Relevance

●●● Strong

Non-React test naming rules are deterministic; no close rejection precedent outweighs the stated
.spec.ts requirement.

PR-#753

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2668286 requires non-React utilities and tests to use .spec.ts, while this PR adds a test
file named dist-loads-in-plain-node.test.ts.

packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-1]
Skill: add-test

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new non-React test uses the prohibited `.test.ts` filename pattern.

## Issue Context
Compliance rule 2668286 requires non-React test files to use the `.spec.ts` suffix. Rename the file while preserving its test contents and ensuring test discovery still includes it.

## Fix Focus Areas
- packages/render-helper/test/dist-loads-in-plain-node.test.ts[1-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 82 rules
✅ Skills: 6 invoked
  add-feature
  add-query
  add-sdk-mutation
  add-test
  code-review
  debug
Review mode: ⚖️ Balanced: This changes node build packaging behavior and adds a substantial process-spawning integration test; the impact is localized but runtime/module-resolution correctness warrants a careful single-pass review.

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/render-helper/test/dist-loads-in-plain-node.test.ts Outdated
Comment thread packages/render-helper/test/dist-loads-in-plain-node.test.ts Outdated
@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes the render-helper Node distribution directly loadable through both ESM and CommonJS by bundling Remarkable only into the Node output.

  • Adds plain-Node import, require, rendering, autolinking, export-map, and packaging tests.
  • Generates third-party license notices from tsup metafiles.
  • Moves output cleanup to the package build script and preserves the separate browser build.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/render-helper/tsup.config.ts Bundles Remarkable exclusively into the Node ESM and CommonJS outputs while leaving the browser dependency policy unchanged.
packages/render-helper/test/dist-loads-in-plain-node.spec.ts Builds isolated output and validates direct Node loading, rendering, dependency inlining, license notices, exports, and metafile cleanup.
packages/render-helper/scripts/third-party-notices.mjs Derives bundled dependency notices from tsup metafiles and removes those build-only metadata files.
packages/render-helper/package.json Updates the package version and makes the build clean output once before generating bundles and third-party notices.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  S[src/index.ts] --> T[tsup]
  T --> B[Browser ESM output]
  T --> N[Node ESM and CJS outputs]
  N --> R[Remarkable bundled]
  T --> M[Metafiles]
  M --> L[Third-party notices]
  N --> P[Plain Node import and require tests]
Loading

Reviews (4): Last reviewed commit: "chore: apply changeset versioning for PR..." | Re-trigger Greptile

Comment thread packages/render-helper/test/dist-loads-in-plain-node.test.ts Outdated
Comment thread packages/render-helper/test/dist-loads-in-plain-node.test.ts Outdated
Bundling remarkable pulls autolinker and tslib in with it. Remarkable and
autolinker are MIT, which requires the copyright and permission notice to travel
with copies of the software, and the emitted bundles carried no notice of any
kind: no @license, no @preserve, no copyright line. That is a licence problem,
not a style one.

The build generates dist/THIRD-PARTY-NOTICES.md from esbuild's own metafile, so
the list is whatever the bundler actually inlined rather than a hand-written one
that drifts. A hand-written list would already have been wrong: remarkable also
depends on argparse, which is tree-shaken out and must not be claimed as
bundled. The metafiles are consumed and deleted so they never reach the tarball,
and npm pack now carries the notices with the full MIT text and both copyright
lines.

The browser build no longer cleans. Cleaning from inside one of two configs only
ever cleaned that config's own directory, left dist/node to accumulate stale
files, and raced with the other build as soon as both were pointed at a single
directory, which is what a test that overrides the output directory does. The
build script clears dist once, before either build runs.

Review round on the test itself:

- renamed to .spec.ts, which is what the other twenty tests in this package use.
- the entry path travels in the environment instead of being interpolated into
  the snippet, so no code is constructed from a value.
- both subprocess cases carry an explicit timeout, rather than a sixty second
  subprocess sitting under vitest's five second default.
- the parsed subprocess output and the parsed manifest are typed.
- added: the notices must exist and cover every inlined package, dist must be
  what npm packs, and no metafile may survive into the output.
Comment thread packages/render-helper/test/dist-loads-in-plain-node.spec.ts Outdated
Both were 60 seconds, so a loaded worker would reach vitest's deadline at the
same moment execFile tried to kill the child, and the failure would be a bare
outer timeout rather than the subprocess error saying what went wrong. The child
gets 30 seconds, the case three times that.
@feruzm feruzm added the patch Bug fixes and patches (1.0.0 → 1.0.1) label Aug 20, 2026
@feruzm
feruzm merged commit ebfd735 into develop Aug 20, 2026
10 checks passed
@feruzm
feruzm deleted the fix/render-helper-node-esm branch August 20, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch Bug fixes and patches (1.0.0 → 1.0.1)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

render-helper: ESM node build cannot be imported by plain Node (directory import of remarkable/linkify)

2 participants