Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/simplelog-otel-entry-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@murky-web/simplelog": patch
---

Fix `@murky-web/simplelog/otel`, which threw on import in 2.0.0.

`dist/otel.js` imported `setSpanContextReader` from `dist/logger_factory.js`, and
that file did not export it:

```
SyntaxError: Export named 'setSpanContextReader' not found in module
.../@murky-web/simplelog/dist/logger_factory.js
```

The build runs two tsdown config blocks — one `platform: "node"`, one
`platform: "neutral"` — that write to the same `outDir`. `logger_factory` was an
entry in neither, so each block emitted it keeping only the exports its own
entries used, and the block that finished last decided the file. The neutral
block's entries (`deno`, `hono`, `web`) never call `setSpanContextReader`, so
when it won, the export was gone. The order is not fixed: rebuilding the broken
config five times produced a working file once and a broken one four times,
which is how this reached npm.

Naming `logger_factory` as an entry in both blocks preserves its full export
surface either way. Verified stable across repeated builds.

A new `smoke:entries` check imports every subpath in the `exports` map from the
built output and fails if one does not load or exports nothing. It runs as part
of `release:verify:simplelog`. Nothing existing could have caught this: lint and
typecheck read the source, and the generated `otel.d.ts` described the export
that the JavaScript was missing — only loading the built files reveals it.
86 changes: 43 additions & 43 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
{
"name": "@murky-web/workspace",
"private": true,
"bin": {
"web-dev-config": "./bin/web-dev-config.mjs"
},
"packageManager": "bun@1.4.0",
"workspaces": [
"packages/*"
],
"scripts": {
"build": "bun run --workspaces --if-present build",
"changeset": "changeset",
"clean": "bun run --workspaces --if-present clean",
"config:init": "bun ./packages/config/bin/web-dev-config.mjs init",
"config:init:solid": "bun ./packages/config/bin/web-dev-config.mjs init --frontend-solid",
"release:verify": "bun run release:verify:typebuddy && bun run release:verify:simplelog && bun run release:verify:oxlint-plugin-solid",
"release:verify:oxlint-plugin-solid": "bun run --cwd ./packages/oxlint-plugin-solid test",
"release:verify:simplelog": "bun run --cwd ./packages/simplelog lint && bun run --cwd ./packages/simplelog typecheck && bun run --cwd ./packages/simplelog test && bun run --cwd ./packages/simplelog smoke:oxlint && bun run --cwd ./packages/simplelog build",
"release:verify:typebuddy": "bun run --cwd ./packages/typebuddy lint && bun run --cwd ./packages/typebuddy typecheck && bun run --cwd ./packages/typebuddy test && bun run --cwd ./packages/typebuddy smoke:globals && bun run --cwd ./packages/typebuddy smoke:oxlint && bun run --cwd ./packages/typebuddy smoke:oxlint:fix && bun run --cwd ./packages/typebuddy smoke:treeshake",
"test": "bun run --workspaces --if-present test",
"typecheck": "bun run --workspaces --if-present typecheck",
"lint": "oxlint -c ./packages/config/oxc/.oxlintrc.jsonc --type-aware ./packages",
"lint:fix": "oxlint -c ./packages/config/oxc/.oxlintrc.jsonc --type-aware --fix ./packages",
"format": "oxfmt -c ./packages/config/oxc/.oxfmtrc.jsonc ./packages",
"format:check": "oxfmt -c ./packages/config/oxc/.oxfmtrc.jsonc --check ./packages",
"publish:jsr": "bun ./scripts/publish-jsr.mjs",
"publish:jsr:dry": "bun ./scripts/publish-jsr.mjs --dry-run",
"publish:npm:dry": "bun ./scripts/publish-npm-dry.mjs",
"release": "bun run release:verify && bun run publish:npm",
"release:dry": "bun run release:verify && bun run publish:npm:dry",
"publish:npm": "changeset publish",
"version-packages": "changeset version && bun ./scripts/sync-jsr-manifests.mjs"
},
"devDependencies": {
"@changesets/changelog-git": "^0.2.1",
"@changesets/cli": "^2.30.0",
"oxfmt": "^0.66.0",
"oxlint": "^1.81.0",
"oxlint-tsgolint": "^7.0.2001",
"tsdown": "0.22.14",
"typescript": "^7.0.2",
"vitest": "3.2.4"
}
"name": "@murky-web/workspace",
"private": true,
"bin": {
"web-dev-config": "./bin/web-dev-config.mjs"
},
"packageManager": "bun@1.4.0",
"workspaces": [
"packages/*"
],
"scripts": {
"build": "bun run --workspaces --if-present build",
"changeset": "changeset",
"clean": "bun run --workspaces --if-present clean",
"config:init": "bun ./packages/config/bin/web-dev-config.mjs init",
"config:init:solid": "bun ./packages/config/bin/web-dev-config.mjs init --frontend-solid",
"release:verify": "bun run release:verify:typebuddy && bun run release:verify:simplelog && bun run release:verify:oxlint-plugin-solid",
"release:verify:oxlint-plugin-solid": "bun run --cwd ./packages/oxlint-plugin-solid test",
"release:verify:simplelog": "bun run --cwd ./packages/simplelog lint && bun run --cwd ./packages/simplelog typecheck && bun run --cwd ./packages/simplelog test && bun run --cwd ./packages/simplelog smoke:oxlint && bun run --cwd ./packages/simplelog smoke:entries",
"release:verify:typebuddy": "bun run --cwd ./packages/typebuddy lint && bun run --cwd ./packages/typebuddy typecheck && bun run --cwd ./packages/typebuddy test && bun run --cwd ./packages/typebuddy smoke:globals && bun run --cwd ./packages/typebuddy smoke:oxlint && bun run --cwd ./packages/typebuddy smoke:oxlint:fix && bun run --cwd ./packages/typebuddy smoke:treeshake",
"test": "bun run --workspaces --if-present test",
"typecheck": "bun run --workspaces --if-present typecheck",
"lint": "oxlint -c ./packages/config/oxc/.oxlintrc.jsonc --type-aware ./packages",
"lint:fix": "oxlint -c ./packages/config/oxc/.oxlintrc.jsonc --type-aware --fix ./packages",
"format": "oxfmt -c ./packages/config/oxc/.oxfmtrc.jsonc ./packages",
"format:check": "oxfmt -c ./packages/config/oxc/.oxfmtrc.jsonc --check ./packages",
"publish:jsr": "bun ./scripts/publish-jsr.mjs",
"publish:jsr:dry": "bun ./scripts/publish-jsr.mjs --dry-run",
"publish:npm:dry": "bun ./scripts/publish-npm-dry.mjs",
"release": "bun run release:verify && bun run publish:npm",
"release:dry": "bun run release:verify && bun run publish:npm:dry",
"publish:npm": "changeset publish",
"version-packages": "changeset version && bun ./scripts/sync-jsr-manifests.mjs"
},
"devDependencies": {
"@changesets/changelog-git": "^0.2.1",
"@changesets/cli": "^2.30.0",
"oxfmt": "^0.66.0",
"oxlint": "^1.81.0",
"oxlint-tsgolint": "^7.0.2001",
"tsdown": "0.22.14",
"typescript": "^7.0.2",
"vitest": "3.2.4"
}
}
1 change: 1 addition & 0 deletions packages/simplelog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"format": "oxfmt -c ../config/oxc/.oxfmtrc.jsonc src oxlint smoke tsdown.config.ts",
"format:check": "oxfmt -c ../config/oxc/.oxfmtrc.jsonc --check src oxlint smoke tsdown.config.ts",
"smoke:oxlint": "bun ./smoke/oxlint/run-smoke.ts",
"smoke:entries": "bun run build && bun ./smoke/entries/run.ts",
"test": "vitest run",
"typecheck": "tsc --project ./tsconfig.json --noEmit"
},
Expand Down
65 changes: 65 additions & 0 deletions packages/simplelog/smoke/entries/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { readFileSync } from "node:fs";
import path from "node:path";

/**
* Imports every published entry point for real.
*
* 2.0.0 shipped a `/otel` entry that threw on import: `dist/otel.js` imported
* `setSpanContextReader` from `dist/logger_factory.js`, which did not export it.
* Nothing caught that, because every other check reads the source. The build emits
* `logger_factory.js` from two tsdown config blocks writing the same `outDir`, and a
* module reached only as a dependency keeps just the exports its own block's entries
* use — so whichever block finished last decided the export list, and that order is
* not fixed. Type-checking the source cannot see it; only loading the built files can.
*
* A `d.ts` next to a `.js` is no proof either: the declaration is generated from the
* source, so it described an export the JavaScript did not have.
*/
const packageRoot = path.resolve(import.meta.dir, "../..");
const pkg = JSON.parse(
readFileSync(path.join(packageRoot, "package.json"), "utf8"),
) as { name: string; exports: Record<string, string | { import?: string }> };

const failures: string[] = [];
let loaded = 0;

for (const [subpath, target] of Object.entries(pkg.exports)) {
if (subpath === "./package.json") {
continue;
}

const file = typeof target === "string" ? target : target.import;
if (file === undefined) {
failures.push(`exports["${subpath}"] has no import condition`);
continue;
}

const resolved = path.join(packageRoot, file);
try {
const module: Record<string, unknown> = await import(resolved);
const names = Object.keys(module).filter((name) => name !== "default");
if (names.length === 0) {
failures.push(`${subpath} -> ${file} loaded but exports nothing`);
continue;
}
loaded += 1;
console.log(` ok ${subpath.padEnd(12)} ${names.join(", ")}`);
} catch (error) {
// An unresolved import between two built files surfaces here and nowhere else.
failures.push(
`${subpath} -> ${file} failed to load: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}

if (failures.length > 0) {
console.error(`\n${failures.length} entry point(s) failed:`);
for (const failure of failures) {
console.error(` ${failure}`);
}
process.exit(1);
}

console.log(`\nEntry smoke passed (${loaded} entry points loaded).`);
11 changes: 11 additions & 0 deletions packages/simplelog/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ export default defineConfig([
entry: {
bun_logger: "./src/bun_logger.ts",
logger: "./src/logger.ts",
// `logger_factory` is shared by entries in both config blocks below, and both
// blocks write it to the same `dist/logger_factory.js`. A module that is only
// reached as a dependency keeps just the exports its own block's entries use,
// so whichever block finished last decided the file's export list — and the
// neutral block does not use `setSpanContextReader`, which left
// `dist/otel.js` importing a binding that was not there. Naming it as an entry
// in both blocks preserves its full surface either way.
logger_factory: "./src/logger_factory.ts",
otel: "./src/otel.ts",
},
dts: true,
Expand All @@ -21,6 +29,9 @@ export default defineConfig([
entry: {
deno_logger: "./src/deno_logger.ts",
hono_middleware: "./src/hono_middleware.ts",
// See the note in the block above: both blocks emit this file, so both have to
// treat it as an entry or the loser truncates it.
logger_factory: "./src/logger_factory.ts",
web_logger: "./src/web_logger.ts",
},
dts: true,
Expand Down
Loading