readme/dev/require_to_import_progress.md
require() → import Migration ProgressTracks the effort to migrate const X = require('...') to typed import statements so values pulled in from other modules carry real types instead of implicit any.
Replace const X = require('...') with a typed import whenever possible and the change is mechanical. require() is fine to keep when the imported module has no types — it already returns implicit any, so no : any or eslint-disable is needed. The aim is to narrow require to the cases where it's actually load-bearing, and let the type checker cover the rest.
For each const X = require('...'):
import when types exist:
.d.ts (check node_modules/<pkg>/package.json for a types field).@types/<pkg> is already installed.@joplin/* package or a relative path to a .ts file.path, fs, etc.) — but confirm the installed @types/node covers the specific submodule (e.g. readline/promises needs ≥ 17).import { foo, bar } from 'pkg' (use { foo as bar } to rename rather than a separate const).module.exports = X style: with esModuleInterop: false (this repo's setting), use import * as X from 'pkg'. import X from 'pkg' only works when the typings declare a real default export.require() in place when:
// @ts-ignore, a new declare module shim, or a fresh @types/* install.require() is needed (RN bundler compatibility, conditional / lazy / side-effect load, dynamic path).: typeof X = require(...) pattern already typed via a paired import type — a deliberate runtime workaround.@types/* packages or upgrade dependencies as part of this migration. The aim is to surface existing types, not expand the dependency graph.CLAUDE.md).import * because the package uses module.exports = ...").yarn tsc --noEmit from the package directory to verify nothing broke. The pre-commit hook handles linting and spellcheck.packages/generator-joplin/generators/app/templates/** — Yeoman template; package lacks a typescript dep.packages/app-cli/tests/support/plugins/*/api/types.ts and similar — regenerated plugin API copies.**/node_modules/** or **/build/**.lib, app-desktop, app-mobile), checkpoint the row every ~20 files.grep -rn --include="*.ts" --include="*.tsx" -E "^\s*const\s+[^=]+=\s*require\(" packages/[name here]/
Counts captured 2026-05-25, before any work. const X = require(...) occurrences in *.ts/*.tsx source files, excluding node_modules/ and build/. Some will legitimately stay as require() after evaluation (untyped libraries, RN bundler workarounds, etc.) — the "Converted" column tracks how many were actually migrated.
| # | Package | Requires (start) | Converted | Remaining | Status |
|---|---|---|---|---|---|
| 1 | pdf-viewer | 1 | 0 | 1 | done (2026-05-25) |
| 2 | fork-uslug | 1 | 0 | 1 | done (2026-05-25) |
| 3 | default-plugins | 1 | 1 | 0 | done (2026-05-25) |
| 4 | editor | 2 | 0 | 2 | done (2026-05-25) |
| 5 | plugin-repo-cli | 2 | 0 | 2 | done (2026-05-25) |
| 6 | htmlpack | 3 | 3 | 0 | done (2026-05-25) |
| 7 | utils | 9 | 4 | 5 | done (2026-05-25) |
| 8 | renderer | 23 | 7 | 16 | done (2026-05-25) |
| 9 | server | 26 | 9 | 17 | done (2026-05-25) |
| 10 | tools | 49 | 29 | 20 | done (2026-05-25) |
| 11 | app-cli | 49 | 18 | 31 | done (2026-05-25) |
| 12 | app-mobile | 61 | 18 | 43 | done (2026-05-25) |
| 13 | app-desktop | 131 | 74 | 57 | done (2026-05-25) |
| 14 | lib | 195 | 55 | 140 | done (2026-05-25) |
| — | generator-joplin | 1 | — | — | excluded (template) |
Total in-scope require() calls at start: 558 (counted across *.ts/*.tsx, excluding **/node_modules/**, **/build/** and **/dist/**).
Smallest first so the workflow stabilises before the large ones:
Each package gets a subsection added below when work begins. Format:
## packages/<name>
Session date: YYYY-MM-DD
Files processed:
- path/to/file.ts — N converted, M left (reasons for skips)
Files skipped entirely:
- path/to/file.ts — reason
Session date: 2026-05-25
Files skipped entirely:
react-dom/client has types and tsc passes the conversion, but the package's build scripts are all disabled (tsc: "", build: "") and the bundling pipeline that produces the vendor/lib/@joplin/pdf-viewer/ artifact isn't traceable from this repo. The author explicitly chose require() to fix a React 18 warning (commit 58da15432). Leave alone until the bundling path is understood.Session date: 2026-05-25
Files skipped entirely:
node-emoji has no types installed.Session date: 2026-05-25
Files processed:
yargs → import * as yargs from 'yargs').Session date: 2026-05-25
Files skipped entirely:
@joplin/lib/string-utils-common, which is a deliberate .js file (no .d.ts) per the comment at the top of that file.Session date: 2026-05-25
Files skipped entirely:
node-fetch is require(...).default with an explicit comment linking the known node-fetch issue; gh-release-assets has no types installed.Session date: 2026-05-25
Files processed:
html-entities).datauri/sync — uses import X = require(...) because the package uses export = X).@joplin/fork-htmlparser2).Session date: 2026-05-25
Files processed:
moment, async-mutex), 1 left (sprintf-js — no @types installed).html-entities, @joplin/fork-htmlparser2).Session date: 2026-05-25
Files processed:
html-entities, @joplin/fork-htmlparser2).html-entities).highlight.js/lib/core).html-entities); 4 left: md5 (no types), @joplin/fork-uslug (types describe { default: fn } but index.js unwraps .default, so types are misaligned with runtime), markdown-it-anchor (no types), ./defaultNoteStyle (JS, no .d.ts).html-entities).html-entities); 2 left: ../urlUtils.js (JS, no .d.ts), font-awesome-filetypes (no types).Files skipped entirely:
markdown-it is paired with import type * as MarkdownItType (deliberate workaround for mobile bundling, per the convention this plan skips).md5 (no types).md5, json-stringify-safe (no types); ./katex_mhchem.js (JS, no .d.ts).md5 (no types); ../../stringUtils.js (JS).md5 (no types).../../urlUtils.js (JS).../../vendor/fountain.min.js (vendor JS).Session date: 2026-05-25
Files processed:
sqlite3); 2 left: @koa/cors (no types), @joplin/lib/shim-init-node.js (lib's .ts source uses module.exports = { ... } rather than ES export, so a typed import {} fails — would require modifying lib to convert).html-entities).pretty-bytes, via import X = require() because the package uses export =).@joplin/lib/string-utils); 1 left (@joplin/lib/database-driver-node.js — JS only, no .d.ts).sqlite3); 1 left (shim-init-node, same reason as app.ts).@joplin/lib/path-utils).query-string, child_process.spawn); 1 left (inline require('child_process').exec inside a function — would require refactoring to top-level).@joplin/lib/string-utils).Files skipped entirely:
require(${__dirname}/packageInfo.js) uses a template-literal path; the line is also already typed via : PackageJson.thirty-two (no types).dayjs and its plugins (advancedFormat, calendar) are convertible, but this is a vendored file (see its header comment) so kept untouched; converting also surfaces a pre-existing noImplicitReturns issue at line 153. later has no types.stripe has types, but the existing call site uses stripeLib(secretKey) as a function; the typed Stripe is a class requiring new Stripe(secretKey, config). Converting would force a logic/signature change.node-cron (no types).prettycron (no types).require('fs') inside a function (would require refactoring to top-level).Session date: 2026-05-25
Files processed:
md5-file).@rmp135/sql-ts merged with existing named import; fs-extra).md5-file, sharp). The typed sharp import surfaced a latent issue at line 682 where s = s.toFile(destPath) assigned a Promise<OutputInfo> to a Sharp variable, which fixed in the same follow-up: replaced with await s.toFile(destPath).yargs-parser)../tool-utils.js merged into the existing import { rootDir }); 1 left (request, no types).path, uri-template, node-fetch). The typed node-fetch import surfaced a latent issue at line 230 where 'Content-Length': binaryBody.length (a number) didn't match HeadersInit's string requirement; fixed in the same follow-up by wrapping in String(...).moment).@joplin/lib/string-utils).glob, path, md5-file).node-fetch, execa, moment); 7 inline require() calls left inside functions (child_process.exec/.spawn, path, https, crypto, fs, readline) — converting needs moving to module top, which is a small refactor.sqlite3); 1 left (@joplin/lib/shim-init-node, same module.exports = { ... } issue as in server).md5-file, @joplin/fork-htmlparser2, style-to-js — drop .default since export = X is callable, crypto).html-entities, @joplin/fork-htmlparser2).moment).@joplin/lib/string-utils).gettext-extractor).Files skipped entirely:
const argv = require('yargs').argv; calls inside main-style async functions (postPreReleasesToForum.ts, tagServerLatest.ts, build-translation.ts, git-changelog.ts, release-android.ts, website/updateNews.ts) — would require a top-level import and a refactor to keep semantics; skipped per the inline-require policy.@joplin/lib/shim-init-node (same as above).require(${baseThemeDir}/${themeFile}) is a dynamic template-literal path.require('../lib/shim') inside a function.gettext-parser (no types).rss (no types).Session date: 2026-05-25
Files processed:
@joplin/lib/path-utils, @joplin/lib/theme).os, @joplin/lib/path-utils).@joplin/lib/locale, @joplin/lib/versionInfo — dropped the .default since import X from resolves the default export)../base-command — TS file with export default class BaseCommand).@joplin/lib/string-utils.js → @joplin/lib/string-utils).sqlite3, @joplin/lib/services/SettingUtils, ./utils/shimInitCli); 2 left: @joplin/lib/database-driver-node.js (JS-only), @joplin/lib/shim-init-node.js (lib uses module.exports = {}). The typed shimInitCli surfaced a latent interface mismatch (ShimInitOptions declares sharp, React, electronBridge, pdfJs as required, runtime defaults them to null); fixed in a follow-up by passing them as null explicitly.chalk, strip-ansi); 3 left (tkwidgets/BaseWidget.js, tkwidgets/framework/termutils.js — no types, ../autocompletion.js — JS).open).Files skipped entirely (sibling command files use the CommonJS module.exports = Command pattern — converting requires changing the exported file too, which is out of scope):
require('./command-X') patterns.Files skipped entirely (other reasons):
./base-command); the typed BaseCommand surfaced a latent issue at line 65 (a variadic log: (...s: string[]) callback spread-passed to single-arg stdout); fixed in the follow-up by narrowing the callback to single-arg. Other requires in the file (@joplin/lib/onedrive-api-node-utils.js JS-only, ./cli-utils.js JS-only, md5 no types) left../cli-utils.js and ./help-utils.js are JS-only.@joplin/lib/Cache is JS-only; line 164 require(\./${path}`)and line 442require('./app-gui.js')` are dynamic/JS.@joplin/lib/services/plugins/sandboxProxy is JS-only.@joplin/lib/shim-init-node.js (same lib issue as elsewhere).tkwidgets/ListWidget.js (no types).require() calls inside functions, or untyped (sprintf-js, md5, fs-extra inline).require(contentScript.path).default.Session date: 2026-05-25
Files processed:
@joplin/lib/path-utils).react-native-file-viewer, default import).@joplin/lib/locale).@joplin/lib/locale, dropped .js extension).react-native-modal-datetime-picker, default).@joplin/lib/string-utils).react-redux, @joplin/lib/locale).@react-native-clipboard/clipboard, default).react-redux).react-redux); several require()s left for internal mobile .js modules (./components/app-nav.js, ./components/screens/onedrive-login.js) and the @joplin/lib/SyncTarget*.js family — TS counterparts either don't exist or coexist with .js build artifacts in ways Metro currently relies on.@react-native-community/push-notification-ios, default, merged with existing import type).react-native named imports).react-native-version-info, default); other requires left (SyncTarget JS-only, AlarmServiceDriver platform extensions kept as require to avoid platform-resolution complications during this mechanical pass).rn-fetch-blob, default); md5 left (no types).Files skipped entirely (latent issues surfaced; reverted for now, worth a follow-up):
react-native-rsa-native returns KeyPair, but the file casts it to a local LegacyRsaKeyPair interface with a missing keySizeBits property. Real interface drift; needs deciding whether the local interface should adapt or the cast is now wrong.@sqlite.org/sqlite-wasm typings only export init (default); the runtime exposes sqlite3Worker1Promiser as a named property. Typed import fails — the .d.ts is incomplete vs the JS. Could be fixed with a local declare module augmentation, but that's beyond a mechanical pass.connect from react-redux initially rejected DropboxLoginScreenComponent because BaseScreenComponent was still require()'d. Fixed in a follow-up: typed both imports, declared Props / State interfaces, and made styles_ / shared_ proper instance fields.Files skipped entirely (other reasons):
require('react') is deliberate (comment explains the timing constraint with shim.setReact).: typeof ShimType = require() is the deliberate paired-with-import type workaround pattern.require('react-native-version-info') and JS-only geolocation-react.js.react-native-dropdownalert (no types); see "Files processed" note for the internal mobile .js requires.color (no types).react-native-paper is required as const { TextInput } = require(...) but the package has no types.@joplin/lib/reserved-ids is JS-only.@joplin/lib/resourceUtils is JS-only.@joplin/turndown, @joplin/turndown-plugin-gfm (no types).md5 / @joplin/whisper-voice-typing no types.@joplin/react-native-alarm-notification (no types).react-native-sqlite-storage (no types).../packageInfo.js (JS-only).path and punycode (mobile-specific bundling; punycode/ has no types).gulp, @joplin/tools/*, @pmmmwh/react-refresh-webpack-plugin, ../babel.config are untyped or build-only).Files skipped entirely:
const X: typeof X = require(...) workarounds for React Native bundler compatibility (already typed via paired import type).tcp-port-used has no types.readline/promises is not in the installed @types/node (16.x); migrating would require a node-types upgrade.Session date: 2026-05-25
This package has the highest concentration of cascading type errors when imports are typed: typed themeStyle / buildStyle from @joplin/lib/theme propagates strict CSSProperties union types (WhiteSpace, TextAlign, OverflowX, etc.) into inline style objects in many screens; typed connect from react-redux rejects components whose base class is still untyped (similar to the BaseScreenComponent case in app-mobile); typed styled-components and reselect.createSelector produce broad downstream errors. To keep this commit mechanical, those four patterns were intentionally not converted here. Many other safe patterns were converted.
Files processed:
@joplin/lib/path-utils); 3 left (@joplin/lib/shim typeof-workaround, fs-extra redundant with existing import, inline electron-window-state).@joplin/lib/path-utils).@electron/remote, electron.clipboard).electron.clipboard); connect, themeStyle left (see note above).electron.ipcRenderer).electron.ipcRenderer).electron.clipboard).electron.clipboard).electron.clipboard).@joplin/lib/string-utils); themeStyle left.electron.clipboard).electron.clipboard).electron.clipboard, @joplin/lib/path-utils, two fs-extra).@joplin/renderer MarkupToHtml).electron.clipboard); @joplin/renderer.utils left (the .utils namespace access doesn't cleanly map to a named import).@joplin/lib/services/CommandService, default).@joplin/lib/string-utils).electron.clipboard).@joplin/lib/urlUtils); react-tooltip reverted because the typed import isn't usable as a JSX component (typings expose only the module namespace); connect left.pretty-bytes); connect, themeStyle left.react-dom/client); connect, Provider, styled-components ThemeProvider/StyleSheetManager/createGlobalStyle left.electron.ipcRenderer).electron.clipboard).@joplin/lib/string-utils).electron.clipboard).pdfjs-dist, is-apple-silicon); @joplin/lib/shim-init-node.js (lib's module.exports = {} issue) left.electron.clipboard); connect left.electron.clipboard, nativeImage).electron.ipcRenderer).Follow-up session (2026-05-25): 5 more safe Node-built-in / typed-package conversions:
path, fs-extra); consolidated with the existing import { resolve } from 'path' so resolve(...) is now path.resolve(...).url).path).fs-extra); folded the existing import { writeFile } from 'fs-extra' into the namespace import.Follow-up session (2026-05-25): full react-redux connect cluster — all 15 connect requires + 2 require('react') requires converted to typed imports. Zero react-redux/react require()s remain in app-desktop.
Files where the conversion was purely mechanical (Props already declared, or class was <any, any> with a matching (props: any) constructor):
connect, Provider part — styled-components left).Files that needed a small typing fix:
<any, any> but the constructor took a narrow Props, so TS inferred Props as the component's prop type. Typed connect then required Props to match the connected/external shape. Fix: typed the class as <Props, State> with concrete State interfaces (Dropbox: { loginUrl, authCode, checkingAuthToken } as written by the dropbox-login-shared.js helper; OneDrive: { authLog: { key, text }[] }). Added dispatch: Dispatch (from redux) to Props (already used by the cancel-button handler). Dropbox's Props.style is typed { width: number; height: number } to match what Navigator.tsx passes to its screens.require('react') and require('react-redux'); the class extended React.Component with no props. Converted to import * as React from 'react', declared Props { themeId, title, id } (all already accessed on this.props), and typed the class as React.Component<Props>.require('react') and require('react-redux'); the class extended React.Component with no props and the constructor called super() with no args. Typed React needs super(props). Declared Props { themeId, apiToken, clipperServer, clipperServerAutoStart } (all accessed in render()), typed the class as React.Component<Props>, and fixed the constructor.Follow-up session (2026-05-25): full @joplin/lib/theme themeStyle / buildStyle cluster — all 9 converted. Seven files were purely mechanical: gui/hooks/useMarkupToHtml.ts, gui/KeymapConfig/styles/index.ts (merged with the existing import { ThemeStyle }), gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx, gui/OneDriveLoginScreen.tsx, gui/TagList.tsx, gui/DropboxLoginScreen.tsx, gui/NoteEditor/NoteEditor.tsx. Two needed inline-style annotations to keep literal types from widening: gui/NoteContentPropertiesDialog.tsx (3 styles annotated React.CSSProperties for textAlign) and gui/ResourceScreen.tsx (3 styles annotated for whiteSpace/overflowX).
Follow-up session (2026-05-25): partial styled-components cluster (7 of 9 + reselect.createSelector).
Mechanical (no propagation):
ThemeProvider, StyleSheetManager, createGlobalStyle).StyleSheetManager).styled, merged with the existing import { css }).styled).styled).styled).styled; the paired styled-system space kept as require() — no types installed).reselect.createSelector; split the selector's return into four React.CSSProperties constants so the literal values for boxSizing/flexDirection etc. stay narrow).Still skipped:
PasswordInput.tsx (its custom ChangeEvent { value: string } shape doesn't match what the <input> actually fires) and a related Function-typed handler chain in SearchInput.tsx. Reverted to require() and flagged in review-later.md for a separate fix.Follow-up session (2026-05-25): typed styled-components in Button.tsx and adjacent fixes. With the underlying typing fixes below in place, every styled-components require() in app-desktop is now converted (9 / 9).
What changed in Button.tsx:
ReactButtonProps from HTMLAttributes<HTMLButtonElement> to ButtonHTMLAttributes<HTMLButtonElement>, which restores the type HTML attribute (<Button type='submit' /> in SsoLoginScreen).SpaceProps interface (m/mt/mr/mb/ml/mx/my/p/pt/pr/pb/pl/px/py, all optional, number | string) and extended Props with it, so <Button mr=… ml=… mb=… /> calls type-check. styled-system has no @types/* package and the migration explicitly avoids adding new types packages — the local interface documents the runtime-supported props.StyledIcon from styled(styled.span(space)) to styled.span<SpaceProps & StyleProps>\${space}; … `so the innermr` prop is in scope on the resulting component (runtime behaviour unchanged).${(props: StyleProps) => props.disabled} template interpolation in StyledButtonPrimary / StyledButtonSecondary. The original (introduced in commit 67f0739d3, Nov 2020) interpolated a boolean into the CSS template: false (most buttons most of the time) produced a bare { … } block whose effect depended on the CSS parser's tolerance of malformed input; true produced an invalid true { … } rule that was dropped. Replaced with &:not(:disabled) { to express the obvious intent ("apply hover/active styles only when not disabled"), and removed the now-dead disabled?: boolean field from StyleProps.Knock-on fixes in callers (made necessary by the typed Button):
gui/ConfigScreen/ButtonBar.tsx — narrowed onCancelClick: Function / onSaveClick?: Function / onApplyClick?: Function to ()=> void (dropped three @typescript-eslint/ban-types disables); typed Button's onClick: ()=> void no longer accepts the broad Function.gui/NoteListControls/NoteListControls.tsx — made StyleProps.padding / StyleProps.buttonVerticalGap optional. They were declared required on the shared StyleProps but only used by StyledRoot; with typed styled(Button), <StyledPairButtonL> calls started failing because they (correctly) don't pass either.styled-system space requires remain in Button.tsx, UserWebviewDialogButtonBar.tsx, and PluginsStates.tsx — the runtime ${space} template still needs the function, and the package has no types.
Files skipped entirely / important categories left untouched:
ChangeEvent { value: string } shape mismatch in PasswordInput.tsx and a Function-typed handler chain in SearchInput.tsx. Flagged in review-later.md for a separate fix.@joplin/lib/services/PluginManager (3), @joplin/lib/onedrive-api-node-utils.js (1), @joplin/lib/markJsUtils (1), @joplin/lib/countable/Countable (1), @joplin/lib/envFromArgs (1), @joplin/lib/components/shared/dropbox-login-shared (1), @joplin/lib/reserved-ids (2), ./packageInfo.js (5), ./services/electron-context-menu (1), ./execCommand (1), ./supportedLocales (1) — JS-only sources.@joplin/lib/shim-init-node.js (2) — same module.exports = { ... } issue described under packages/server.md5 (4), debounce (5), color (2), styled-system (3), taboverride (1), source-map-support (1), react-toggle-button (1), formatcoords (1), gulp (1), @joplin/tools/* (3) — no types installed.require() calls inside functions / arrow callbacks (bridge.ts, mockClipboard.ts, markdownEditor.spec.ts, ElectronAppWrapper.ts electron-window-state) — would require moving to top level.Session date: 2026-05-25
Many conversions, but several modules surfaced latent type issues that need their own follow-up:
Categories converted (across roughly 40 files):
./path-utils, ./string-utils, ./urlUtils, ./markdownUtils, ./errorUtils, ./locale (when paired with a TS source).path, os, url, events (where it doesn't cascade), https, http, crypto, buffer, zlib, timers, dgram, querystring — namespace or destructured.async-mutex (incl. the .Mutex access pattern), html-entities (.AllHtmlEntities), chokidar, fast-deep-equal, query-string, markdown-it, md5-file, string-to-stream, @joplin/renderer (MarkupToHtml), sqlite3.Latent issues surfaced & reverted (worth follow-ups):
moment in time.ts: typed moment() rejects the string | number | Date | { toDate(): Date } union — anythingToDateTime / anythingToMs rely on narrowing o past { toDate(): Date } via 'toDate' in o, but TS doesn't propagate the narrowing into the moment(o, format) call. Fixed in a follow-up by converting to a typed namespace import and adding a const input = o as string | number | Date cast for the moment calls. services/interop/InteropService_Importer_Md_frontmatter.test.ts keeps require('moment') for now.fs-extra in fs-driver-node.ts: typed fs.appendFile(path, content, { encoding }) and fs.writeFile(path, content, { encoding }) don't accept { encoding: string } — they expect { encoding: BufferEncoding }. Fixed in a follow-up by converting the require() to a typed namespace import and casting encoding as BufferEncoding at the three internal fs call sites; the public method signatures stay as string to match the base class.reselect.createSelectorCreator / defaultMemoize and re-reselect.createCachedSelector (in reducer.ts, services/commands/MenuUtils.ts, services/commands/ToolbarButtonUtils.ts): typed createCachedSelector returns a different signature than the codebase assumes — selectArrayShallow(props, cacheKey) then errors with Expected 1 arguments, but got 2. Probably the reselect version was upgraded without updating call sites.events.EventEmitter in services/plugins/Plugin.ts: typed EventEmitter.on(event, fn) rejects Function-typed callbacks. Fixed in a follow-up: tighten the two callbacks to (...args: unknown[]) => void, switch to a typed import { EventEmitter } from 'events', drop the two @typescript-eslint/ban-types disables and the InstanceType<typeof EventEmitter> workaround.@joplin/lib/string-utils formatCssSize in services/style/themeToCss.ts: same latent issue as the desktop's useThemeCss.ts — formatCssSize(value: string) was called with number. Fixed in a follow-up by widening the signature to string | number (the body already handled both at runtime); both call sites then convert cleanly to typed imports.Files skipped entirely (other reasons):
@joplin/lib/shim-init-node (multiple places) and JS-only sibling files (./reserved-ids, ./database-driver-node.js, ./file-api.js, ./file-api-driver-onedrive.js, ./file-api-driver-webdav, ./randomClipperPort, ./import-enex-md-gen.js, ./import-enex-html-gen.js, ./resourceUtils.js, ./SyncTarget*.js, ./parameters.js, ./vendor/*, ./welcomeAssets) — JS-only, no TS counterpart.sprintf-js (14), md5 (9), url-parse (3), tcp-port-used (2), string-padding (2), @joplin/fork-sax (2), word-wrap, uglifycss, server-destroy, relative, node-notifier, multiparty, image-data-uri, hpagent, diff-match-patch, base64-stream, base-64, @joplin/turndown, @joplin/turndown-plugin-gfm, @aws-sdk/client-s3, @adobe/css-tools, better-sqlite3, sharp, color, @joplin/fork-uslug.require() inside functions / .default accesses on JS-only modules (shim-init-node.ts has many of these inside shimInit).electron requires inside shim-init-node.ts (lib used in node + electron; the require('electron').nativeImage calls inside conditional branches are deliberately late-loaded).EventDispatcher.test.ts line 125: kept as require(...).default because the test specifically asserts that the require + .default pattern works.