diff --git a/.changeset/commonjs-require.md b/.changeset/commonjs-require.md new file mode 100644 index 0000000..9bd2e7b --- /dev/null +++ b/.changeset/commonjs-require.md @@ -0,0 +1,5 @@ +--- +"@maple-dev/effect-orm": patch +--- + +Every entry point can be loaded with `require()`. The export map offered only an `import` condition, so CommonJS code (and CommonJS builds of TypeScript projects) failed with `ERR_PACKAGE_PATH_NOT_EXPORTED`, although the modules themselves load fine through Node's `require(esm)` (Node 20.19+, 22.12+). Each entry now also has a `default` condition pointing at the same `.mjs`, so a CommonJS caller loads the very module an ES module caller does, and shares its Effect. The package-consumer check `require()`s every entry from the packed tarball. diff --git a/README.md b/README.md index 8e2714c..8d13f2e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ See [Getting started](./docs/getting-started.md) for source builds and examples. newer Effect 4 releases without opting into Effect 5. Effect 3 and the Effect 4 prereleases are incompatible. +The package is ESM. CommonJS code can `require()` every entry point on Node 20.19+ +or 22.12+ (`require(esm)`), the same way it loads Effect 4. + ## Quick start One import per database. Each holds the whole query builder plus that database's column diff --git a/docs/getting-started.md b/docs/getting-started.md index e636449..3b41e16 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,8 +5,11 @@ Build and decode your first query without connecting to a database. Then follow ## Installation and compatibility -This is an ESM-only TypeScript package built on **Effect 4**. You do not need an +This is an ESM TypeScript package built on **Effect 4**. You do not need an Effect application: `Effect.runPromise` lets you use it from ordinary async code. +CommonJS code can `require()` every entry point too, on Node 20.19+ or 22.12+, which +load ES modules synchronously (`require(esm)`). That is the same way such code loads Effect 4, +which is also ESM-only, so the library and its caller share one Effect. These examples are checked against Effect `4.0.0` and the builder in this repository. The package declares `effect ^4.0.0` as a peer dependency; Effect 3 is incompatible. diff --git a/package.json b/package.json index cf43063..0ec0f98 100644 --- a/package.json +++ b/package.json @@ -35,51 +35,63 @@ "exports": { "./clickhouse": { "types": "./dist/clickhouse.d.mts", - "import": "./dist/clickhouse.mjs" + "import": "./dist/clickhouse.mjs", + "default": "./dist/clickhouse.mjs" }, "./expr": { "types": "./dist/expr.d.mts", - "import": "./dist/expr.mjs" + "import": "./dist/expr.mjs", + "default": "./dist/expr.mjs" }, "./sql": { "types": "./dist/sql.d.mts", - "import": "./dist/sql.mjs" + "import": "./dist/sql.mjs", + "default": "./dist/sql.mjs" }, "./postgres": { "types": "./dist/postgres.d.mts", - "import": "./dist/postgres.mjs" + "import": "./dist/postgres.mjs", + "default": "./dist/postgres.mjs" }, "./schema": { "types": "./dist/schema.d.mts", - "import": "./dist/schema.mjs" + "import": "./dist/schema.mjs", + "default": "./dist/schema.mjs" }, "./tinybird": { "types": "./dist/tinybird.d.mts", - "import": "./dist/tinybird.mjs" + "import": "./dist/tinybird.mjs", + "default": "./dist/tinybird.mjs" }, "./migrate": { "types": "./dist/migrate.d.mts", - "import": "./dist/migrate.mjs" + "import": "./dist/migrate.mjs", + "default": "./dist/migrate.mjs" }, "./database": { "types": "./dist/database.d.mts", - "import": "./dist/database.mjs" + "import": "./dist/database.mjs", + "default": "./dist/database.mjs" }, "./kit": { "types": "./dist/kit.d.mts", - "import": "./dist/kit.mjs" + "import": "./dist/kit.mjs", + "default": "./dist/kit.mjs" }, "./benchmark": { "types": "./dist/benchmark/index.d.mts", - "import": "./dist/benchmark/index.mjs" + "import": "./dist/benchmark/index.mjs", + "default": "./dist/benchmark/index.mjs" }, "./benchmark/http": { "types": "./dist/benchmark/http.d.mts", - "import": "./dist/benchmark/http.mjs" + "import": "./dist/benchmark/http.mjs", + "default": "./dist/benchmark/http.mjs" }, "./benchmark/cli": { "types": "./dist/benchmark/cli.d.mts", - "import": "./dist/benchmark/cli.mjs" + "import": "./dist/benchmark/cli.mjs", + "default": "./dist/benchmark/cli.mjs" } }, "publishConfig": { diff --git a/scripts/check-package.ts b/scripts/check-package.ts index 651b2be..f9bb490 100644 --- a/scripts/check-package.ts +++ b/scripts/check-package.ts @@ -92,6 +92,9 @@ const program = Effect.gen(function* () { yield* runCommand("node", ["consumer-ts5.mts"], temporary) // The script runs on Bun; the published library must also work in Node. yield* runCommand("node", ["consumer.mts"], temporary) + // And from CommonJS, through `require(esm)`: every entry, from the real tarball. + yield* fs.copyFile(path.join(root, "tests/package-consumer.cjs"), path.join(temporary, "consumer.cjs")) + yield* runCommand("node", ["consumer.cjs"], temporary) yield* runCommand( path.join(temporary, "node_modules/.bin/ch-bench"), ["schema", "--json"], diff --git a/tests/package-consumer.cjs b/tests/package-consumer.cjs new file mode 100644 index 0000000..a5621f6 --- /dev/null +++ b/tests/package-consumer.cjs @@ -0,0 +1,36 @@ +// Copied into an isolated directory by check-package.ts and run with Node. +// A CommonJS consumer: every public entry must load through `require()`, which +// Node (20.19+, 22.12+) satisfies with `require(esm)` once the export map offers +// a condition other than `import`. Nothing here may resolve dependencies or +// source files from Maple's workspace. +const assert = require("node:assert/strict") +const { readFileSync } = require("node:fs") +const { dirname, join } = require("node:path") + +// The package does not export its own package.json, so read the installed copy +// through an entry it does export. Driving the list from `exports` covers every +// entry added later without touching this file. +const root = dirname(dirname(require.resolve("@maple-dev/effect-orm/clickhouse"))) +const { exports: exportMap } = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) +const entries = Object.keys(exportMap) +assert.ok(entries.length > 0, "the package exports no entries") +for (const entry of entries) { + const specifier = `@maple-dev/effect-orm${entry.slice(1)}` + const loaded = require(specifier) + assert.ok(Object.keys(loaded).length > 0, `${specifier} loaded with no exports`) +} + +// And it works, not just loads: compile and decode with the same Effect a CJS +// host resolves, so the library and its caller share one Effect instance. +const { Effect } = require("effect") +const CH = require("@maple-dev/effect-orm/clickhouse") +const events = CH.table("events", { external: true, columns: { id: CH.uint64, name: CH.string } }) +const query = CH.from(events) + .select(($) => ({ id: CH.toString($.id), name: $.name })) + .where(($) => [$.name.eq(CH.param.string("name"))]) +const compiled = Effect.runSync(CH.compile(query, { name: "maple" })) +assert.match(compiled.sql, /FROM events/) +assert.deepEqual(Effect.runSync(compiled.decodeRows([{ id: "18446744073709551615", name: "maple" }])), [ + { id: "18446744073709551615", name: "maple" }, +]) +console.log(`CommonJS consumer ok (${entries.length} entries)`)