Skip to content

Add type-strip mode: run Harper directly from .ts sources#562

Draft
kriszyp wants to merge 69 commits into
mainfrom
claude/typestrip-mode
Draft

Add type-strip mode: run Harper directly from .ts sources#562
kriszyp wants to merge 69 commits into
mainfrom
claude/typestrip-mode

Conversation

@kriszyp

@kriszyp kriszyp commented May 16, 2026

Copy link
Copy Markdown
Member

Summary

Lets Harper run from the same source tree in two modes:

  • node bin/harper.ts — Node 24's built-in type-strip (ESM, runs .ts directly)
  • node dist/bin/harper.js — existing CJS dist build (unchanged shipped artifact)

Both reach Harper successfully started. for the daemon, and produce identical output for version / help.

Why

Running .ts directly removes the build step from the inner dev loop and gives us a path to drop the tsc-CJS emit eventually. The challenge is that Harper's runtime module graph is full of CJS↔ESM cycles and module-load side effects (config reads, server-singleton wiring, listener registrations, class-extends-Resource) that work under tsc's synchronous CJS but TDZ under Node's ESM evaluation.

Approach (three commits)

  1. Add startup-phase lifecycle and runtime path helpers — new utility/lifecycle.ts (onStartup(cb) / runStartup()) plus RUNTIME_SRC_ROOT / RUNTIME_FILE_EXT exports from utility/packageUtils.js so callers can distinguish source vs. dist at runtime without __dirname/import.meta shenanigans.
  2. Convert CJS .js source modules to ESM .ts — 57 mechanical renames (requireimport, module.exportsexport, etc.). This is the bulk of the diff.
  3. Wire type-strip entry point and defer module-load side effectsbin/harper.ts reshaping, worker bootstrap, onStartup wrappers, late-binding for Resource subclasses, defensive env.get, removal of eager module-top env.initSync() calls.

Where to focus review attention

Higher-confidence:

  • The lifecycle module itself (utility/lifecycle.ts) — small, contained.
  • RUNTIME_SRC_ROOT/RUNTIME_FILE_EXT additions to packageUtils.js — purely additive.
  • The mechanical .js.ts conversion in commit 2.

Lower-confidence — please scrutinize:

  1. Late-bound Resource subclassesresources/ErrorResource.ts, security/certificateVerification/{certificateVerificationSource,crlVerification}.ts, and the Login class in resources/login.ts now create their class inside onStartup() and export via export let X. In tsc's CJS emit, this becomes exports.X = void 0 then exports.X = class … inside the hook. Caveat for future contributors: module-top destructuring of these exports (const { ErrorResource } = require('./ErrorResource')) would capture undefined. All current consumers access via mod.X at use-site (or destructure inside a function body), which sees the live property after startup — but this is a sharp edge worth knowing about.

  2. Defensive env.get / getHdbBasePath in utility/environment/environmentManager.ts — both now tolerate being called before this module's body has fully run (catch ReferenceError from configUtils.getConfigValue TDZ; optional-chain installProps?.). Belt-and-suspenders for cycle timing; could mask a real "called before init" bug. Now that initEnv() always runs before any subcommand module loads, this might be removable in a follow-up.

  3. isEntry check in bin/harper.ts:

    const isEntry = typeof module === 'undefined' || require.main === module;

    Works because nothing currently imports bin/harper.ts as a module. If anyone ever does (test runner, wrapper script), the CLI side-effect would run unexpectedly. Hardening this would need either import.meta.url (which tsc errors on under CJS emit) or a separate ESM/CJS entry shim.

  4. Worker RUNTIME_FILE_EXT switch in manageThreads.startWorker() — the function now accepts extensionless module identifiers ('server/threads/threadServer') and resolves them against RUNTIME_SRC_ROOT with .ts or .js per mode. Verify this matches how external consumers call startWorker.

  5. Removed eager module-top env.initSync() calls (22 sites). Initialization is centralized in bin/harper.ts (initEnv) and threadServer.ts for workers. Tests that import these modules directly will need to call initSync() themselves.

Test implications

  • The startup-phase split changes when side effects run. Unit tests that depend on server.X being wired, env.get(K) returning a real value, or worker-thread message listeners being registered must call runStartup() (exported from utility/lifecycle.ts) before exercising those paths. resetStartupForTests() is provided for inter-test isolation.

Verification

$ node bin/harper.ts version    → 5.1.0
$ node bin/harper.ts help       → full usage text
$ node bin/harper.ts            → daemon starts, "Harper successfully started." (4 workers, no errors)
$ npm run build                 → tsc emits dist/ (pre-existing type-check warnings unrelated to this PR)
$ node dist/bin/harper.js version  → 5.1.0
$ node dist/bin/harper.js          → daemon starts, "Harper successfully started."
$ npm run lint:required         → 0 errors, 0 warnings
$ npm run format:check          → clean

Process notes

  • Diff is large (162 files, +708/-8372) but the bulk is the deterministic .js.ts rename in commit 2; commits 1 and 3 are where the actual design decisions live and warrant the closest read.
  • Cross-model review done via gemini before pushing; its flags addressed inline above.

🤖 Generated by Claude Code (Opus 4.7, 1M context).

kriszyp and others added 3 commits May 16, 2026 17:13
Introduces utility/lifecycle.ts with onStartup(cb) / runStartup() so
side-effectful module-load work (server-singleton wiring, listener
registration, config-derived constants) can be deferred to a controlled
startup phase that runs after env.initSync() and before request handling.
This is the lever that makes ESM-cycle TDZs avoidable without restructuring
the import graph.

Adds RUNTIME_SRC_ROOT and RUNTIME_FILE_EXT to packageUtils.js. Detects
whether the running modules are .ts sources (type-strip) or .js dist files
by inspecting packageUtils.js's own __dirname, so callers like
manageThreads.startWorker() can spawn workers with the correct file path
under both modes from a single string identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the 57 .js source files that participate in Harper's runtime
module graph to .ts and rewrites them in ESM form. The driving constraint:
under Node's type-strip mode, .ts files with import/export syntax are
loaded as ESM, and any CJS .js file that require()s a .ts ESM file in the
entry's evaluation chain throws ERR_REQUIRE_CYCLE_MODULE.

Mechanical edits applied across all converted files:
- const X = require('mod')               → import X from 'mod'
- const { A, B } = require('mod')        → import { A, B } from 'mod'
- const X = require('mod').default       → import X from 'mod'
- module.exports = { a, b, ... }         → export { a, b, ... }
- module.exports = X                     → export default X
- 'use strict' pragma removed (ESM default)
- Relative imports given explicit .ts/.js extensions

These files keep their existing internal structure and behavior; the
conversion is syntactic, not semantic. Behavioral changes (deferred
side-effects, late-binding, etc.) live in the follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the runtime support needed to run Harper directly from .ts sources
under Node's type-strip mode (node bin/harper.ts) while keeping the
existing CJS dist build (node dist/bin/harper.js) working unchanged.

Entry point (bin/harper.ts):
- Switch cases use await import('./mod.ts') instead of require('./mod')
- Splits boot into initEnv() (always cheap) and runServerStartup() (only
  for start/run paths); version/help skip the heavy init
- Replaces require.main === module with a dual-mode isEntry check

Worker thread bootstrap (server/threads/threadServer.ts):
- Workers run env.initSync() → runStartup() → startServers() in an async
  IIFE so they see a populated env and complete onStartup hooks before
  serving requests

manageThreads.startWorker():
- Accepts an extensionless module identifier and appends RUNTIME_FILE_EXT
- Resolves against RUNTIME_SRC_ROOT (source dir under type-strip,
  dist/ under CJS dist)
- listenersByType / messageListeners lazy-initialized inside the accessor
  so callers don't trip TDZ when manageThreads is loaded mid-cycle

Late-binding for class-extends-Resource modules:
- ErrorResource, CertificateVerificationSource,
  CertificateRevocationListSource, Login: the class is constructed
  inside onStartup() and exposed via `export let X` so consumers see a
  live binding after startup. These modules sit inside Resource.ts's own
  static-graph SCC, so a class-extends declaration at module-top TDZ'd.

Module-load side-effect deferrals (one-off onStartup wrappers):
- server.X = … assignments in security/user.ts, security/auth.ts,
  resources/analytics/write.ts, server/http.ts,
  server/serverHelpers/serverUtilities.ts, server/threads/threadServer.ts
- Object.defineProperty(server, …) in server/nodeName.ts and
  server/threads/manageThreads.ts
- onMessage* listener registrations in server/threads/itc.ts,
  security/keys.ts, resources/analytics/write.ts,
  utility/processManagement/processManagement.ts, bin/restart.ts
- Config-derived constants in security/auth.ts and
  server/serverHelpers/contentTypes.ts converted to lazy getters
- server.contentTypes attachment in contentTypes.ts deferred
- whenComponentsLoaded-awaiting IIFE in
  server/DurableSubscriptionsSession.ts deferred

Environment manager (utility/environment/environmentManager.ts):
- env.get() and getHdbBasePath() made defensive against being called
  before this module's body has run (catches TDZ ReferenceError from
  configUtils.getConfigValue and returns undefined). Belt-and-suspenders
  for the rare timing window where a module-load access reaches in
  before initSync completes.
- Removed 22 eager module-top env.initSync() calls; initialization is
  now centralized in bin/harper.ts (initEnv) and threadServer.ts
- Top-level let/const → var so subsequent functions don't TDZ on
  module-internal state if invoked through the cycle

harper_logger.ts:
- RootConfigWatcher import moved from static (which forced a
  configUtils → logger cycle) to dynamic, inside the function that
  actually constructs the watcher
- logRotator require replaced with dynamic import inside its setTimeout
- module.exports = {…} wrapped in `if (typeof module !== 'undefined')`
  so the CJS-shaped legacy export survives tsc emit but is skipped
  under ESM, where the parallel export default already covers the
  default-import consumers

Compatibility fixes:
- Type-only imports tagged via tsc --verbatimModuleSyntax sweep
- CJS-only packages (lodash, fs-extra, micromatch, papaparse, etc.)
  switched to default-import-and-destructure
- JSON imports tagged with `with { type: 'json' }`
- Extensionless relative imports given explicit .ts/.js extensions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp requested review from a team as code owners May 16, 2026 23:21
`(mod.default || mod)()` only worked when `await import()` returned an
ESM-shaped namespace (mod.default = function). In tsc-compiled CJS dist,
`await import('./install.js')` from a CJS file wraps the CJS exports as
mod.default, so the actual function is at mod.default.default. CI's
`Setup Harper` step (which runs the dist build) hit this with
`TypeError: (mod.default || mod) is not a function`.

Adds a small getDefaultExport() helper that walks both shapes and applies
it to the INSTALL, STOP, and STATUS cases in bin/harper.ts.

Verified locally:
- node bin/harper.ts install  → reaches installer
- node dist/bin/harper.js install → reaches installer
- node bin/harper.ts version  → 5.1.0
- node bin/harper.ts          → "Harper successfully started."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread bin/run.ts Outdated
Comment thread server/threads/threadServer.ts
@claude

claude Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

2 blockers still open — the node-forge default-export fix (d2cf35e) is correct and introduces no new issues.

Blocker 1 — unitTests/dataLayer/bulkLoad.test.js:230 — stale rewire binding (unchanged)

dataLayer/bulkLoad.js was renamed to bulkLoad.ts. tsc's CJS emit names the binding PermissionResponseObject_ts_1, not _js_1. The __set__ call targets a nonexistent key, the stub is never installed, and the isHDBError(result) === true assertion at line 241 is a false pass.

Fix: bulkLoad_rewire.__set__('PermissionResponseObject_ts_1', { default: PermissionResponseObject_rw })

Blocker 2 — unitTests/resources/caching.test.js:371cache.clear() crash on RocksDB (unchanged)

primaryStore.cache is undefined on the RocksDB path (the default). Every other call site in Table.ts uses cache?.get / cache?.delete, but this test calls .clear() without the optional chain, causing a TypeError that kills the test runner on the primary storage path.

Fix: CachingTable.primaryStore.cache?.clear()

Issues caught by the claude/review bot and the unit-test job:

1. bin/run.ts:235 (`launch()`) — `require('../utility/processManagement/processManagement.ts')` would throw `ReferenceError` under typestrip ESM. Converted to `await import(...)` (mirrors `filterArgsAgainstRuntimeConfig` a few lines above).

2. server/threads/threadServer.ts (`onSocket`) — `require('../../components/componentLoader.ts').getComponentName` had the same problem on the secure-port code path. Converted onSocket to async and used `await import(...)`. Inspector calls in the same file (debug-mode paths) now use a single static `import * as inspector from 'node:inspector'` instead of per-call requires, so they work in both modes.

3. utility/functions/sql/alaSQLExtension.ts — `import mathjs from 'mathjs'` got `undefined` for `mathjs.mad` etc. since mathjs is ESM-only and has no default export. Switched to `import * as mathjs` and replaced the trailing `module.exports = {...}` with `export default {...}` so the file is consistently ESM. This was the cause of `TypeError: Cannot read properties of undefined (reading 'mad')` that surfaced in unit-test runs.

Verified both modes still reach "Harper successfully started."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp force-pushed the claude/typestrip-mode branch from 23a2e4d to ded6937 Compare May 17, 2026 00:18
Comment thread server/threads/socketRouter.ts Outdated
Comment thread bin/upgrade.ts Outdated
Addresses the second round of claude/review findings plus the
ErrorResource regression Gemini flagged that showed up in unit tests:

- server/threads/socketRouter.ts:31 — `await require('./threadServer.ts').startServers()` was half-converted (extension updated but `require` left); switched to `await (await import(...)).startServers()`. Triggered when threadCount === 0 in typestrip mode.

- bin/upgrade.ts:36 — `pm2Utils = require('../utility/processManagement/processManagement.ts')` parallel to the `pmUtils` fix already done in bin/run.ts; switched to `await import(...)`. Was breaking `harper upgrade` in typestrip mode.

- utility/signalling.ts — `serverItcHandlers = serverItcHandlers || require('../server/itc/serverHandlers.ts')` in signalSchemaChange/signalUserChange; both functions made async and switched to `await import(...)`.

- server/fastifyRoutes/plugins/hdbCore.js — renamed to .ts and converted to ESM. As a CJS file it required serverHandlers.ts (now ESM), throwing ERR_REQUIRE_CYCLE_MODULE under typestrip when fastify routes load. The single importer (server/fastifyRoutes.ts) updated to point at .ts.

- resources/ErrorResource.ts — rewrote to lazy-construct the class on first
  use via a Proxy. The earlier `onStartup`-based late binding worked in
  production (where runStartup runs before componentLoader can construct
  ErrorResource) but broke unit tests that exercise componentLoader without
  going through the lifecycle, producing the `ErrorResource is not a
  constructor` failure. The Proxy resolves on `new` or property access, so
  the class is always available regardless of lifecycle state, and still
  dodges the original module-load TDZ on Resource.

Verified locally:
- node bin/harper.ts version → 5.1.0
- node bin/harper.ts → Harper successfully started.
- node dist/bin/harper.js → Harper successfully started.
- npm run lint:required → 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread dataLayer/harperBridge/lmdbBridge/lmdbMethods/lmdbCreateAttribute.ts Outdated
Harper's unit tests use `rewire` to override module-internal bindings
via `__set__` / `__get__`. Rewire works by `eval`-injecting code into
the module's closure, so it needs a variable with the exact name the
test references.

`tsc`'s CJS emit renames `import X from 'mod'` to a `_X_ts_1` alias
internally and rewrites all uses to `_X_ts_1.default`. After commit
1f4522a converted the underlying source files from CJS `.js` to ESM
`.ts`, the dist `_ts_1` aliases broke ~95 rewire-using unit tests
that expected to find a variable named `X` in scope.

This commit adds a `const X = _X` alias right after each affected
default/namespace/named import in the 21 source files that
rewire-using tests target. tsc emits this as a real module-scope
`const X = _X_ts_1.default;` (or equivalent), restoring the variable
name rewire looks for. ESM (typestrip) sees the same `const X = _X;`
trivially.

Files touched (each only adds the alias for the specific names the
test rewires; the rest of the imports are left alone):

  bin/upgrade.ts
  components/operations.ts
  config/configUtils.ts
  dataLayer/bulkLoad.ts
  dataLayer/delete.ts
  dataLayer/export.ts
  dataLayer/insert.ts
  dataLayer/readAuditLog.ts
  dataLayer/schema.ts
  dataLayer/schemaDescribe.ts
  dataLayer/update.ts
  dataLayer/harperBridge/lmdbBridge/lmdbMethods/{lmdbCreateSchema,lmdbCreateTable,lmdbDropTable,lmdbUpsertRecords}.ts
  security/fastifyAuth.ts
  server/serverHelpers/serverHandlers.ts
  sqlTranslator/sql_statement_bucket.ts
  validation/role_validation.ts
  validation/schemaMetadataValidator.ts
  validation/validationWrapper.ts

Sampled local results after applying:
- upgrade.test.js:           4 passing (was: fail in before hook)
- validationWrapper.test.js: 8 passing (was: ReferenceError validate not defined)
- insert.test.js:           16 passing
- delete.test.js:           12 passing
- sql_statement_bucket.test.js: 16 passing
- configUtils.test.js:      40 passing, 1 unrelated failure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp force-pushed the claude/typestrip-mode branch from 2b250f2 to 31b0ef6 Compare May 17, 2026 02:22
Comment thread dataLayer/harperBridge/lmdbBridge/lmdbMethods/lmdbCreateAttribute.ts Outdated
kriszyp and others added 2 commits May 16, 2026 21:34
Round of follow-up fixes after the typestrip conversion exposed several
test failures in the dist (CJS-emit) build:

- utility/functions/geo.ts: turfArea, turfLength, turfCircle,
  turfDistance, turfBooleanContains, turfBooleanEqual,
  turfBooleanDisjoint were called as `.default(...)` (CJS interop
  pattern) but the new ESM `import X from '@turf/X'` already resolves to
  the default. Removed the `.default` accessors. Also switched
  `@turf/helpers` to `import * as` since it only exposes named exports.
  (geo unit tests now pass: 79/79)

- server/serverHelpers/contentTypes.ts: my earlier rsync was off a
  pre-x-ndjson revision of main, so the rsync overwrote main's
  application/x-ndjson handler. Restored it.
  (contentTypes test: 16/16 passing)

- utility/signalling.ts: my prior fix made signalSchemaChange /
  signalUserChange async (to `await import('serverHandlers.ts')`),
  but the existing tests and callers assumed sync. Restored sync
  signatures and moved the import to an `onStartup` preload + safe
  optional-chain fallback for environments where startup never runs.
  (signalling tests: 4/4 passing)

- utility/install/checkJWTTokensExist.ts: original CJS used
  `module.exports = fn` so `require('./checkJWTTokensExist')(...)` was
  callable directly. My `export default fn` converted to
  `exports.default = fn`, breaking that calling convention. Added a
  conditional `module.exports = fn` shim that runs only when `module`
  is defined (tsc CJS emit) and is skipped under typestrip ESM.
  (checkJWTTokensExist tests: 2/2 passing)

- unitTests/testUtils.js: `preTestPrep()` now calls
  `lifecycle.runStartup()` after `initTestEnvironment()`. Many tests
  stub server-singleton methods (e.g. `sinon.stub(server, 'getUser')`)
  that are now wired during the startup phase rather than at module
  load. Calling runStartup() in preTestPrep gives tests the same
  fully-wired state production sees. (`runStartup` is idempotent.)
  (auth.test.js: 10/10 passing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…taller)

The rewire-target extractor missed two patterns in the first pass:
- Variable-based rewire calls like `rewire(MODULE_PATH)` where MODULE_PATH is
  a local string constant
- The harper_logger PropertiesReader import (via the requireUncached helper)

Applies the same const-alias-after-import pattern to:
- utility/logging/harper_logger.ts: PropertiesReader
- utility/install/installer.ts: PropertiesReader, installValidator, mountHdb,
  checkJwtTokens

Local check: harper_logger.test.js now 24/24 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread launchServiceScripts/launchHarperDB.ts Outdated
Comment thread dataLayer/harperBridge/lmdbBridge/lmdbUtility/LMDBCreateAttributeObject.ts Outdated
Comment thread dataLayer/harperBridge/lmdbBridge/lmdbMethods/lmdbSearchByValue.ts Outdated
Comment thread dataLayer/harperBridge/lmdbBridge/lmdbMethods/lmdbSearchByConditions.ts Outdated
The runtime-env-vars test used `__set__('require', stub)` to intercept the
inline `require('./harperConfigEnvVars.ts')` call in
`applyRuntimeEnvVarConfig`. The ESM conversion hoisted that require to a
top-level import, leaving the test stub unable to substitute anything and
the function ran against the real fs (which then errored on `/test/root`).

Adds the const-alias pattern for `applyRuntimeEnvConfig` in configUtils.ts
so rewire can patch the binding directly, and updates the test to
`__set__('applyRuntimeEnvConfig', stub)` instead of the old require-trap.
13/13 in that test file passing now.
Comment thread launchServiceScripts/launchHarperDB.ts Outdated
kriszyp and others added 2 commits May 17, 2026 00:24
Two issues uncovered while investigating integration-test regressions:

- dataLayer/{insert,delete,readAuditLog}.ts imported `harperBridge` via
  `import _harperBridge ...; const harperBridge = _harperBridge;`. Under
  the existing intentional cycle (insert.ts ↔ harperBridge.ts via the
  bridge's transitive deps), `_harperBridge` is in TDZ at the moment
  the alias evaluates, so `harperBridge` was bound to `undefined`
  forever and the daemon crashed at startup with
  `Cannot access '_harperBridge' before initialization`. Replaced the
  capture with a Proxy that defers `.X` reads to call time — the cycle
  is fully resolved by the time any usage fires, and rewire tests can
  still patch the binding name.

- server/jobs/jobRunner.ts used `join(__dirname, './jobProcess.js')` to
  resolve the job-worker entry path. `__dirname` is undefined under
  type-strip ESM, so the call threw silently when a job was launched
  (which silently meant CSV/bulk-load jobs never spawned a worker).
  Switched to an extensionless identifier `'server/jobs/jobProcess'`
  so `manageThreads.startWorker()` resolves it against `RUNTIME_SRC_ROOT`
  with the correct extension per execution mode.

Note: integration-test CSV bulk-load is still broken locally — the job
worker startup hangs without reaching the actual data path. The above
fixes are necessary preconditions but not sufficient; ongoing
investigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mports

claude/review flagged that the LMDB bridge files still had top-level
\`require('./path').default || require('./path')\` shims that throw
\`ReferenceError: require is not defined\` under typestrip ESM. Same in
the launchHarperDB entry script and mount_hdb's createTables.

Converted via regex sweep — pattern was uniform across these files:
\`const X = require('./path').default || require('./path');\`
becomes \`import X from './path';\`. Where the require pattern was
spread across multiple lines (triple-require defensive shims in
lmdbCreateTransactionsAuditEnvironment), collapsed to a single import.

launchServiceScripts/launchHarperDB.ts: top-level \`require(...)\` call
replaced with a dynamic \`import(...).then(...)\`.

utility/mount_hdb.ts: inline require inside \`createTables\` hoisted to
a top-of-file import (CreateTableObject).

Verified \`node bin/harper.ts version\` still works. \`npm run lint:required\`
clean, \`npm run format:write\` no changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread dataLayer/harperBridge/lmdbBridge/lmdbMethods/lmdbSearchByConditions.ts Outdated
The destructured SearchByConditionsObject/SearchCondition were only
referenced from JSDoc @param annotations — under typestrip mode the
top-level require() throws ReferenceError. The TS file is already
typed; the JSDoc-only imports add no value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp requested review from Ethan-Arrowood and cb1kenobi and removed request for a team May 27, 2026 02:01
@cb1kenobi

Copy link
Copy Markdown
Member

Trying to test this with Node 24 and running node bin/harper.ts causes the error:

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///Users/chris/projects/harper/json/systemSchema.json" needs an import attribute of "type: json"
    at validateAttributes (node:internal/modules/esm/assert:88:15)
    at defaultLoadSync (node:internal/modules/esm/load:168:3)
    at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:770:12)
    at #loadSync (node:internal/modules/esm/loader:790:49)
    at ModuleLoader.load (node:internal/modules/esm/loader:756:26)
    at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:488:31)
    at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:549:36)
    at afterResolve (node:internal/modules/esm/loader:597:52)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:603:12)
    at ModuleJob.syncLink (node:internal/modules/esm/module_job:163:33) {
  code: 'ERR_IMPORT_ATTRIBUTE_MISSING'
}

There's a couple places where we import .json files and I believe we need the import thing from 'file.json' with { type: 'json' }. It's also complaining that the "type": "module" is not set in the package.json.

Static JSON imports (`import x from './file.json'`) require
`with { type: 'json' }` in Node 24 ESM/typestrip mode, but TypeScript
with module:NodeNext + CJS output raises TS2856 if the attribute is
present.

Replace all six static systemSchema JSON imports with synchronous
`JSON.parse(readFileSync(...))` using `PACKAGE_ROOT` from
`utility/packageUtils.js` for path resolution. This pattern:
  - Works in typestrip mode (ESM, Node 24)
  - Works in CJS dist (Node 20+)
  - Emits no TS2856 errors
  - Is consistent with how other JSON files are loaded in the codebase

Files changed: utility/globalSchema.ts, utility/mount_hdb.ts,
security/user.ts, dataLayer/.../lmdbSearch.ts,
dataLayer/.../lmdbDropAttribute.ts, upgrade/directives/5-2-0.ts

Closes reviewer feedback from cb1kenobi.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented May 29, 2026

Copy link
Copy Markdown
Member Author

Fixed in e44864a. Replaced all six static import x from './systemSchema.json' calls with JSON.parse(readFileSync(join(PACKAGE_ROOT, 'json/systemSchema.json'), 'utf-8')) — this works in both typestrip/ESM (Node 24) and CJS dist without needing with { type: 'json' } (which TypeScript rejects when targeting CommonJS). The PACKAGE_ROOT from utility/packageUtils.js handles path resolution in both modes.

The "type": "module" issue: without it, Node 24 type-stripping still runs because it detects the ESM import/export syntax — adding it to the root package.json would also require a dist/package.json with "type": "commonjs" to preserve the CJS dist, which is a larger change we can follow up separately.

Claude

kriszyp and others added 4 commits May 29, 2026 15:17
The unit test was injecting `systemSchema_json_1` (the CJS name tsc
emits for `import systemSchema from './file.json'`). The previous commit
replaced the static JSON import with `const systemSchema = JSON.parse(…)`,
so the rewire target is now `systemSchema` directly. Update the test to
match and drop the `{ default: … }` wrapper that the import helper added.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Single conflict in components/operations.ts: kept ESM imports from
HEAD and picked up main's new `awaitDeploymentRow` export from
deploymentRecorder.ts + `onPeerResult` arg to replicateOperation.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Conflicts in 5 files (serverHandlers.ts, manageThreads.ts,
threadServer.ts, operationsServer.ts, fastifyRoutes.ts) — all the same
pattern: main introduced `workerProcessGuard.ts` with a `realExit()`
wrapper around process.exit() for cleaner worker shutdown. Kept our ESM
imports, added `import { realExit }` and replaced `process.exit(0)` with
`realExit(0)` in the SHUTDOWN handler.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Conflict in server/itc/serverHandlers.ts: main added named exports for
userHandler and schemaHandler (for MCP listChanged subscribers). Kept
our ESM `export default` and converted the new exports to ESM style.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Comment thread server/threads/threadServer.ts Outdated
startServers();
})().catch((err) => {
harperLogger.fatal('Worker failed to start', err);
process.exit(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker (new) — fatal worker startup error is now silently swallowed

workerProcessGuard.ts is imported at line 18 of this file, so by the time this catch block runs in a worker thread the guard has already replaced process.exit with a logging stub. process.exit(1) here is intercepted — the worker logs the fatal error but continues running in a broken state instead of terminating.

Every other fatal exit in this commit (fastifyRoutes.ts, jobProcess.ts, operationsServer.ts) was updated to realExit() — this one was missed.

Suggested change
process.exit(1);
realExit(1);

The review caught that the fatal worker startup catch block still called
process.exit(1) instead of realExit(1). Since workerProcessGuard.ts is
imported at the top of threadServer.ts, process.exit is already replaced
by the guard's logging stub by the time this catch fires — the worker
would log the error but continue running in a broken state.

Every other fatal exit in this file was already updated in the merge
commit; this one was missed.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented May 30, 2026

Copy link
Copy Markdown
Member Author

Fixed comment 3329139859 in c57a656 — changed process.exit(1) to realExit(1) in the worker startup catch block. Good catch; all other fatal exits in threadServer.ts were already updated in the merge commit but this one slipped through. — Claude

@cb1kenobi

Copy link
Copy Markdown
Member

First try I get:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/chris/projects/harper/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js' imported from /Users/chris/projects/harper/upgrade/directives/5-2-0.ts
    at finalizeResolution (node:internal/modules/esm/resolve:271:11)
    at moduleResolve (node:internal/modules/esm/resolve:865:10)
    at defaultResolve (node:internal/modules/esm/resolve:992:11)
    at #cachedDefaultResolve (node:internal/modules/esm/loader:691:20)
    at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:708:38)
    at ModuleLoader.resolveSync (node:internal/modules/esm/loader:740:52)
    at #resolve (node:internal/modules/esm/loader:673:17)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:593:35)
    at ModuleJob.syncLink (node:internal/modules/esm/module_job:163:33)
    at ModuleJob.link (node:internal/modules/esm/module_job:253:17) {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///Users/chris/projects/harper/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js'
}

kriszyp and others added 2 commits June 1, 2026 10:09
Static and dynamic imports of TypeScript source files were still using
.js extensions in several files, causing ERR_MODULE_NOT_FOUND when
running under Node 24 type-strip mode (node bin/harper.ts).

Files fixed:
  - upgrade/directives/5-2-0.ts: initializePaths.js → .ts
  - components/deployLifecycle.ts: manageThreads.js → .ts
  - components/mcp/index.ts: configUtils.js → .ts
  - server/operationsServer.ts: configUtils.js, itc.js, manageThreads.js → .ts
  - security/certificateVerification/certificateVerificationSource.ts:
    crlVerification.js, ocspVerification.js → .ts (dynamic imports)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The previous commit ran `git add` without GIT_WORK_TREE set, causing
git to resolve the path relative to the git-dir root instead of the
worktree root. Only certificateVerificationSource.ts was tracked
correctly (the sed-i broke its hard-link, giving it a new inode that
git could resolve). The other 8 files in the directory appeared deleted
in the index because git couldn't open the directory.

Re-add all files with GIT_WORK_TREE explicitly set to restore the
directory to the committed state.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

Fixed in 927c9d3 + d724e76. Converted 6 remaining .js import extensions to .ts in TypeScript source files (upgrade/directives/5-2-0.ts, components/deployLifecycle.ts, components/mcp/index.ts, server/operationsServer.ts, and the two dynamic imports in certificateVerificationSource.ts). The initial fix commit accidentally dropped certificateVerificationSource.ts from the git index due to a worktree path-resolution quirk — that was restored in the follow-up commit.

Claude

The previous .js→.ts commit used sed -i which breaks hard links by
creating a new inode. When git add ran without GIT_WORK_TREE set, git
resolved the wrong worktree root and staged an older version of
operationsServer.ts (from the main checkout's hard-linked copy),
accidentally reverting:
  - env.initSync() try/catch wrapper
  - `type` keyword on fastify imports
  - requestTimePlugin.ts and serverHandlers.ts extensions
  - operationsServer default parameter `= {}`

The correct on-disk versions of all 4 affected files are now staged
using GIT_WORK_TREE=$(pwd) to ensure the worktree root resolves
correctly in this submodule worktree setup.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@cb1kenobi

Copy link
Copy Markdown
Member

Getting closer:

There was an error during install. Exiting. ReferenceError: require is not definedError creating role
    at checkSchemaExists (file:///Users/chris/projects/harper/utility/common_utils.ts:544:27)
    at Module.checkSchemaTableExist (file:///Users/chris/projects/harper/utility/common_utils.ts:527:23)
    at Module.insertData (file:///Users/chris/projects/harper/dataLayer/insert.ts:131:39)
    at Module.addRole (file:///Users/chris/projects/harper/security/role.ts:88:15)
    at async createAdminUser (file:///Users/chris/projects/harper/utility/install/installer.ts:578:18)
    at async createSuperUser (file:///Users/chris/projects/harper/utility/install/installer.ts:620:2)
    at async install (file:///Users/chris/projects/harper/utility/install/installer.ts:190:2)
    at async initialize (file:///Users/chris/projects/harper/bin/run.ts:98:4)
    at async Module.main (file:///Users/chris/projects/harper/bin/run.ts:203:3)

In typestrip mode (node bin/harper.ts), `require` is undefined in ESM
modules. common_utils.ts had 6 lazy `require('../resources/databases')`
calls inside function bodies to break circular-import cycles.

Replace with a top-level ESM import of getDatabases and a thin wrapper
function. The circular dependency is safe because these functions are
only called at runtime, never during module initialization.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

Fixed in 7d8b12d. The 6 require('../resources/databases') lazy calls in utility/common_utils.ts were replaced with a top-level ESM import { getDatabases as _getDatabases } and a thin wrapper function. The circular dependency is safe because these functions are only called at runtime, never during module initialization.

Claude

common_utils.ts → databases.ts → itc.ts → common_utils.ts created
a circular dependency that caused the 'restart' ITC listener to not
register on the main thread, breaking restart_service jobs (workers
couldn't restart HTTP workers after component hot-reload).

Fix: inline isEmpty() directly in itc.ts (it was the only function
itc.ts used from common_utils) so itc.ts no longer imports common_utils.
This breaks the cycle and allows common_utils.ts to safely add a
top-level import of getDatabases from databases.ts.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@cb1kenobi

Copy link
Copy Markdown
Member
[error]: TypeError: Error creating user - TypeError: validate.cleanAttributes is not a function
    at Module.addUser (file:///Users/chris/projects/harper/security/user.ts:136:27)
    at createAdminUser (file:///Users/chris/projects/harper/utility/install/installer.ts:592:18)
    at async createSuperUser (file:///Users/chris/projects/harper/utility/install/installer.ts:620:2)
    at async install (file:///Users/chris/projects/harper/utility/install/installer.ts:190:2)
    at async initialize (file:///Users/chris/projects/harper/bin/run.ts:98:4)
    at async Module.main (file:///Users/chris/projects/harper/bin/run.ts:203:3)

kriszyp and others added 2 commits June 1, 2026 11:01
…level import

Previous approach (direct top-level import) created circular deps with
multiple modules: databases.ts → Table.ts / auditStore.ts / DatabaseTransaction.ts
→ common_utils.ts, all of which import back into common_utils.ts.

New approach:
  - utility/databasesRef.ts: tiny dep-free registry with setDatabasesGetter()
    and getDatabases()
  - resources/databases.ts: calls setDatabasesGetter(() => getDatabases()) at
    module top-level, after its own getDatabases export is defined
  - utility/common_utils.ts: imports getDatabases from databasesRef.ts (no cycle)
  - server/threads/itc.ts: inlines isEmpty to remove its import of common_utils
    (that import would have created a cycle via databases→itc→common_utils)

This eliminates all `require()` calls from common_utils.ts while keeping the
lazy-loading semantics needed to avoid circular dependency issues.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
In ESM (node bin/harper.ts), `import _validate from 'validate.js'`
returns the CJS module wrapped as `{ default: validateFn }`. The old
`import * as validate from 'validate.js'` therefore left `validate`
pointing at the namespace object, making `validate.cleanAttributes`
undefined.

Use `_validate.default ?? _validate` to handle both ESM (where .default
is the library) and CJS dist (where _validate already IS the library).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

Fixed in 3ae1a25. In ESM typestrip mode, import _validate from 'validate.js' wraps the CJS export as { default: validateFn }, so cleanAttributes wasn't on the namespace. Now using (_validate as any).default ?? _validate to unwrap the default export in ESM while staying backward-compatible with CJS dist.

Claude

@cb1kenobi

Copy link
Copy Markdown
Member

This PR is still broken.

[error]: TypeError: Cannot read properties of undefined (reading 'publicKeyFromPem')
    at generateKeys (file:///Users/chris/projects/harper/security/keys.ts:406:18)
    at async Module.generateCertsKeys (file:///Users/chris/projects/harper/security/keys.ts:504:36)
    at async install (file:///Users/chris/projects/harper/utility/install/installer.ts:194:2)
    at async initialize (file:///Users/chris/projects/harper/bin/run.ts:98:4)
    at async Module.main (file:///Users/chris/projects/harper/bin/run.ts:203:3)

Please move this PR back to draft. It's not ready for review.

Same pattern as validate.js: node-forge is CJS and gets wrapped as
{ default: forge } when imported in ESM. `import * as forge` then
returns the namespace instead of the library, making forge.pki
undefined and causing `publicKeyFromPem` to fail during install.

Use `_forge.default ?? _forge` to unwrap the default export in ESM
while staying backward-compatible with the CJS dist.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@kriszyp

kriszyp commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

Fixed in d2cf35e. Same CJS default-export wrapping issue as validate.js: node-forge is CJS and import * as forge in ESM typestrip mode gives the namespace object, not the library, so forge.pki was undefined. Used _forge.default ?? _forge to unwrap correctly.

Also moving the PR back to draft as requested while continuing to track and fix remaining issues.

Claude

@kriszyp kriszyp marked this pull request as draft June 1, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants