Skip to content

Full migratione from cjs to es#1573

Open
AStaroverov wants to merge 3 commits into
mainfrom
feat/no-more-cjs
Open

Full migratione from cjs to es#1573
AStaroverov wants to merge 3 commits into
mainfrom
feat/no-more-cjs

Conversation

@AStaroverov

@AStaroverov AStaroverov commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Greptile Summary

This PR completes the migration of 97 files across the monorepo from CommonJS to ESM, changing the default createRolldownNodeConfig output from ["es", "cjs"] to ["es"], adding "type": "module" to package manifests, polyfilling __dirname in CLI entry points, and replacing the hand-written dual-format resolve-helper files with a single TypeScript source that implements a two-stage CJS→ESM resolver. The most notable functional change is the replacement of Zod-based GlobalOverviewReg schema validation with a manual migration function that skips inner-field validation.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/robustness concerns that do not affect current functionality.

All three inline comments are P2: dead code in the ESM resolver, a loss of deep runtime validation (intentional trade-off for removing Zod from the parse path), and a future fragility in require.resolve for model packages that don't yet have exports fields. No regressions in the primary migration path are evident.

lib/node/resolve-helper/src/index.ts (dead code), tools/block-tools/src/v2/registry/schema_public.ts (validation regression)

Important Files Changed

Filename Overview
lib/node/resolve-helper/src/index.ts New TypeScript ESM resolver: replaces hand-written CJS/ESM dual files with a full implementation; dead code on line 181 ("default" condition checked twice).
tools/block-tools/src/v2/registry/schema_public.ts Replaces Zod-based GlobalOverviewReg schemas with plain TypeScript types + manual migration; loses runtime validation of id and description fields.
tools/block-tools/src/cmd/build-model.ts Migrates from synchronous require() to createRequire().resolve() + dynamic import() for ESM support; fragile for future pure-ESM model packages with exports field.
tools/ts-builder/src/configs/utils/createRolldownNodeConfig.ts Default output formats changed from ["es", "cjs"] to ["es"] — the central ESM-only change for the build infrastructure.
tools/block-tools/bin/run.js Added standard __filename/__dirname ESM polyfill needed by oclif.execute({ dir: __dirname }).
lib/node/resolve-helper/package.json Moved from hand-written src/index.js/.mjs dual output to a proper ts-builder-built ESM package; added standard build scripts and engine requirement node >= 22.
tools/block-tools/package.json Migrated from Vite dual CJS/ESM build to ts-builder ESM-only build; updated oclif target path from ./dist/cli.js to ./dist/cmd/index.js.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/node/resolve-helper/src/index.ts
Line: 181

Comment:
**Dead code — `"default"` is already handled by the `CONDITIONS` loop**

`CONDITIONS` is `["node", "import", "default"]`, so when the loop at lines 175–180 runs, it already tries `"default"` and — if found — calls `resolveConditions(obj.default, subpath)`. If that call returns `undefined`, the loop finishes and falls through; the `if ("default" in obj)` guard on line 181 is then entered, but the call returns exactly the same `undefined` value. This line can never alter the final result and is unreachable in any meaningful way.

```suggestion
  return undefined;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: tools/block-tools/src/v2/registry/schema_public.ts
Line: 98-119

Comment:
**Loss of runtime validation for required fields**

The previous Zod-based `GlobalOverviewReg.parse()` validated every field including `id`, description objects (`BlockPackDescriptionManifest`), and SHA256 hashes, throwing a structured error on any mismatch. The new `migrateGlobalOverviewEntry` just casts these with `as`:

- `id: o.id as BlockPackIdNoVersion` — if `o.id` is `undefined` or malformed, the cast succeeds but downstream accesses on `id.organization` / `id.name` will throw cryptic `TypeError`s rather than a clear parse error.
- `description` values inside `latestByChannel` entries are not checked against `BlockPackDescriptionManifest` at all.

Consider adding at least a guard for the `id` field, e.g.:

```typescript
if (!o.id || typeof o.id !== "object")
  throw new Error(`Invalid global overview entry: missing or invalid id`);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: tools/block-tools/src/cmd/build-model.ts
Line: 48-51

Comment:
**`require.resolve` may fail for pure-ESM model packages with an `exports` field**

`createRequire(import.meta.url).resolve(modulePath)` uses the CJS resolution algorithm, which only honours `require` / `node` export conditions. If a model package under `flags.modulePath` exposes only an `import` condition (e.g. `"exports": { ".": { "import": "./dist/index.js" } }`), `require.resolve` will throw `ERR_PACKAGE_PATH_NOT_EXPORTED` and the `import()` call is never reached.

The current model packages (e.g. `enter-numbers/model`) rely solely on `"main"` (no `exports` field), so this works today. But as packages migrate further, this becomes fragile. Using `import.meta.resolve` (Node ≥ 20.6) or the workspace's own `tryResolve` helper from `@milaboratories/resolve-helper` would handle both CJS and ESM exports correctly.

```typescript
// Safer alternative (Node ≥ 20.6 or polyfilled via resolve-helper):
const resolved = await import.meta.resolve(pathToFileURL(modulePath).href);
let { model, platforma } = await import(resolved);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "full migration from cjs to es" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

@changeset-bot

changeset-bot Bot commented Apr 20, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4a09cbd

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request implements a comprehensive migration of the codebase to ECMAScript Modules (ESM). Key changes include updating package.json files to specify module types, refactoring source files from CommonJS to ESM syntax, and introducing a new resolve-helper utility for cross-environment package resolution. The review feedback focuses on the resolve-helper implementation, specifically addressing a bug in subpath wildcard replacement, improving error code accuracy for unexported paths, and suggesting a naming convention update for constants.

const middle = subpath.slice(prefix.length, subpath.length - (suffix?.length ?? 0));
const target = resolveConditions(obj[key], subpath);
if (typeof target !== "string") return target;
return target.replace("*", middle);

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.

medium

Node.js subpath patterns allow the * wildcard to be used multiple times in the exports target string. Using replace only substitutes the first occurrence. Use replaceAll to ensure all instances are correctly replaced, which is the standard behavior in Node.js resolution.

Suggested change
return target.replace("*", middle);
return target.replaceAll("*", middle);

}
}
}
return subpath === "." ? undefined : null;

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.

medium

When the exports field is present in package.json, any subpath that is not explicitly matched should result in an ERR_PACKAGE_PATH_NOT_EXPORTED error rather than MODULE_NOT_FOUND. In this implementation, returning undefined from matchExports leads to MODULE_NOT_FOUND in esmResolve, while null correctly signals the export error. This fallback (and others at lines 115, 123, and 159) should return null to better align with Node's resolution algorithm.

Suggested change
return subpath === "." ? undefined : null;
return null;

return undefined;
}

const selfRequire = createRequire(import.meta.url);

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.

medium

According to the general rules, constants should use SCREAM_CASE or UpperCamelCase. Please rename selfRequire to follow this convention (e.g., SELF_REQUIRE). Note that you will also need to update its usage on line 189.

Suggested change
const selfRequire = createRequire(import.meta.url);
const SELF_REQUIRE = createRequire(import.meta.url);
References
  1. Use SCREAM_CASE or UpperCamelCase for constants.

import * as blockTools from '@platforma-sdk/block-tools';

async function loadBlockDescription() {
const __dirname = dirname(fileURLToPath(import.meta.url));

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.

import.meta.dirname couldn't be used here?

if (res !== undefined) return res;
}
}
if ("default" in obj) return resolveConditions(obj.default, subpath);

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.

P2 Dead code — "default" is already handled by the CONDITIONS loop

CONDITIONS is ["node", "import", "default"], so when the loop at lines 175–180 runs, it already tries "default" and — if found — calls resolveConditions(obj.default, subpath). If that call returns undefined, the loop finishes and falls through; the if ("default" in obj) guard on line 181 is then entered, but the call returns exactly the same undefined value. This line can never alter the final result and is unreachable in any meaningful way.

Suggested change
if ("default" in obj) return resolveConditions(obj.default, subpath);
return undefined;
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/node/resolve-helper/src/index.ts
Line: 181

Comment:
**Dead code — `"default"` is already handled by the `CONDITIONS` loop**

`CONDITIONS` is `["node", "import", "default"]`, so when the loop at lines 175–180 runs, it already tries `"default"` and — if found — calls `resolveConditions(obj.default, subpath)`. If that call returns `undefined`, the loop finishes and falls through; the `if ("default" in obj)` guard on line 181 is then entered, but the call returns exactly the same `undefined` value. This line can never alter the final result and is unreachable in any meaningful way.

```suggestion
  return undefined;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +98 to 119
function migrateGlobalOverviewEntry<D>(raw: unknown): GlobalOverviewEntry<D> {
if (!raw || typeof raw !== "object")
throw new Error(`Invalid global overview entry: ${JSON.stringify(raw)}`);
const o = raw as Record<string, unknown> & Partial<GlobalOverviewEntry<D>>;
const allVersionsWithChannels =
o.allVersionsWithChannels ?? (o.allVersions ?? []).map((v) => ({ version: v, channels: [] }));
const latestByChannel = { ...(o.latestByChannel ?? {}) };
if (!latestByChannel[AnyChannel] && o.latest !== undefined && o.latestManifestSha256) {
latestByChannel[AnyChannel] = {
description: o.latest as D,
manifestSha256: o.latestManifestSha256,
};
}
return {
...o,
id: o.id as BlockPackIdNoVersion,
allVersionsWithChannels,
latest: o.latest as D,
latestManifestSha256: (o.latestManifestSha256 ?? "") as string,
latestByChannel,
};
}

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.

P2 Loss of runtime validation for required fields

The previous Zod-based GlobalOverviewReg.parse() validated every field including id, description objects (BlockPackDescriptionManifest), and SHA256 hashes, throwing a structured error on any mismatch. The new migrateGlobalOverviewEntry just casts these with as:

  • id: o.id as BlockPackIdNoVersion — if o.id is undefined or malformed, the cast succeeds but downstream accesses on id.organization / id.name will throw cryptic TypeErrors rather than a clear parse error.
  • description values inside latestByChannel entries are not checked against BlockPackDescriptionManifest at all.

Consider adding at least a guard for the id field, e.g.:

if (!o.id || typeof o.id !== "object")
  throw new Error(`Invalid global overview entry: missing or invalid id`);
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/block-tools/src/v2/registry/schema_public.ts
Line: 98-119

Comment:
**Loss of runtime validation for required fields**

The previous Zod-based `GlobalOverviewReg.parse()` validated every field including `id`, description objects (`BlockPackDescriptionManifest`), and SHA256 hashes, throwing a structured error on any mismatch. The new `migrateGlobalOverviewEntry` just casts these with `as`:

- `id: o.id as BlockPackIdNoVersion` — if `o.id` is `undefined` or malformed, the cast succeeds but downstream accesses on `id.organization` / `id.name` will throw cryptic `TypeError`s rather than a clear parse error.
- `description` values inside `latestByChannel` entries are not checked against `BlockPackDescriptionManifest` at all.

Consider adding at least a guard for the `id` field, e.g.:

```typescript
if (!o.id || typeof o.id !== "object")
  throw new Error(`Invalid global overview entry: missing or invalid id`);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +48 to +51
const require = createRequire(import.meta.url);
const resolved = require.resolve(modulePath);
// eslint-disable-next-line prefer-const, @typescript-eslint/no-unsafe-assignment
let { model, platforma } = require(modulePath);
let { model, platforma } = await import(pathToFileURL(resolved).href);

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.

P2 require.resolve may fail for pure-ESM model packages with an exports field

createRequire(import.meta.url).resolve(modulePath) uses the CJS resolution algorithm, which only honours require / node export conditions. If a model package under flags.modulePath exposes only an import condition (e.g. "exports": { ".": { "import": "./dist/index.js" } }), require.resolve will throw ERR_PACKAGE_PATH_NOT_EXPORTED and the import() call is never reached.

The current model packages (e.g. enter-numbers/model) rely solely on "main" (no exports field), so this works today. But as packages migrate further, this becomes fragile. Using import.meta.resolve (Node ≥ 20.6) or the workspace's own tryResolve helper from @milaboratories/resolve-helper would handle both CJS and ESM exports correctly.

// Safer alternative (Node ≥ 20.6 or polyfilled via resolve-helper):
const resolved = await import.meta.resolve(pathToFileURL(modulePath).href);
let { model, platforma } = await import(resolved);
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/block-tools/src/cmd/build-model.ts
Line: 48-51

Comment:
**`require.resolve` may fail for pure-ESM model packages with an `exports` field**

`createRequire(import.meta.url).resolve(modulePath)` uses the CJS resolution algorithm, which only honours `require` / `node` export conditions. If a model package under `flags.modulePath` exposes only an `import` condition (e.g. `"exports": { ".": { "import": "./dist/index.js" } }`), `require.resolve` will throw `ERR_PACKAGE_PATH_NOT_EXPORTED` and the `import()` call is never reached.

The current model packages (e.g. `enter-numbers/model`) rely solely on `"main"` (no `exports` field), so this works today. But as packages migrate further, this becomes fragile. Using `import.meta.resolve` (Node ≥ 20.6) or the workspace's own `tryResolve` helper from `@milaboratories/resolve-helper` would handle both CJS and ESM exports correctly.

```typescript
// Safer alternative (Node ≥ 20.6 or polyfilled via resolve-helper):
const resolved = await import.meta.resolve(pathToFileURL(modulePath).href);
let { model, platforma } = await import(resolved);
```

How can I resolve this? If you propose a fix, please make it concise.

@codecov

codecov Bot commented Apr 20, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
357 1 356 6
View the top 1 failed test(s) by shortest run time
src/core/client.test.ts > test client init
Stack Traces | 0.0656s run time
RpcError: failed to commit user root creation: cannot commit: a conflict occurred
 ❯ ClientReadableStreamImpl.<anonymous> ../../../node_modules/.pnpm/@protobuf-ts+grpc-transport@2.11.1_@grpc+grpc-js@1.13.4/node_modules/@.../build/commonjs/grpc-transport.js:85:52
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client.ts:612:17
 ❯ next ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:149:26
 ❯ Object.onReceiveStatus src/core/ll_client.ts:422:15
 ❯ InterceptingListenerImpl.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/call-interface.ts:145:18
 ❯ Object.onReceiveStatus ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/client-interceptors.ts:419:47
 ❯ ../../../node_modules/.pnpm/@grpc+grpc-js@1.13.4/node_modules/@.../grpc-js/src/resolving-call.ts:169:23

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { code: 'INTERNAL', meta: { 'content-type': 'application/grpc' }, methodName: 'ListUserResources', serviceName: 'MiLaboratories.PL.API.Platform' }

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

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