Skip to content

Commit 0f42319

Browse files
committed
chore(release): v0.16.2
1 parent ccd558d commit 0f42319

9 files changed

Lines changed: 175 additions & 21 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ jobs:
8282
- name: Validate npm package contents
8383
run: npm pack --dry-run
8484

85+
- name: Run npm tests
86+
run: npm test
87+
8588
- if: env.RELEASE_PUBLISH != 'true'
8689
name: Run publish dry-runs
8790
run: |

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
node_modules/
1+
node_modules/
2+
dist/

AGENTS.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Run these after changing package metadata or CI/package validation behavior.
2121

2222
1. `deno run -A scripts/sync-package-metadata.ts`: rewrite `deno.json` from
2323
`package.json` metadata.
24-
2. `deno task validate`: verify formatting, lint, tests, JSR dry-run, and npm
25-
pack dry-run.
24+
2. `deno task validate`: verify formatting, lint, tests, JSR dry-run, npm pack
25+
dry-run, and a Node install/import smoke test.
2626
3. `npm install --no-package-lock --ignore-scripts`: refresh local dev
2727
dependencies without introducing a lockfile. This is required before local
2828
`deno task validate`, `npm x vitest run`, and `bun x vitest run` commands.
@@ -33,12 +33,16 @@ Operational commands are documented here so `deno.json` only carries the main
3333
aggregate validation entrypoint.
3434

3535
1. `deno task validate`: verify metadata sync, format, lint, tests, JSR dry-run,
36-
and npm pack dry-run.
36+
npm pack dry-run, and the Node smoke test.
3737
2. `deno x vitest run`: run the Vitest suite through Deno.
38-
3. `npm x vitest run`: run the Vitest suite in Node.
39-
4. `bun x vitest run`: run the Vitest suite in Bun.
40-
5. `deno x vitest bench --run`: run the Vitest benchmark suite.
41-
6. `act -W .github/workflows/ci.yml`: run the CI workflow locally when `act` is
38+
3. `npm test`: run the Node-side Vitest suite and the npm install/import smoke
39+
test.
40+
4. `npm x vitest run`: run the Vitest suite in Node.
41+
5. `bun x vitest run`: run the Vitest suite in Bun.
42+
6. `npm run smoke:npm`: pack the current tree, install it into a temporary Node
43+
project, and verify the installed package imports successfully.
44+
7. `deno x vitest bench --run`: run the Vitest benchmark suite.
45+
8. `act -W .github/workflows/ci.yml`: run the CI workflow locally when `act` is
4246
installed.
4347

4448
## Releases

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.16.2 — 2026-04-05
4+
5+
- Fix npm package imports under Node by shipping JavaScript entrypoints instead
6+
of relying on TypeScript-only package resolution from `node_modules`.
7+
38
## 0.16.1 — 2026-04-02
49

510
- Align release publishing so npm and JSR stay version-synchronized.

deno.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "@rotu/structview",
3-
"version": "0.16.1",
3+
"version": "0.16.2",
44
"license": "MIT",
55
"tasks": {
6-
"validate": "deno run -A scripts/sync-package-metadata.ts --check && deno fmt --check && deno lint && deno x vitest run && deno publish --dry-run --allow-dirty && npm pack --dry-run"
6+
"validate": "deno run -A scripts/sync-package-metadata.ts --check && deno fmt --check && deno lint && deno x vitest run && deno publish --dry-run --allow-dirty && npm pack --dry-run && npm run smoke:npm"
77
},
88
"fmt": {
99
"semiColons": false
@@ -12,6 +12,7 @@
1212
"publish": {
1313
"include": [
1414
"deno.json",
15+
"dist",
1516
"bigendian.ts",
1617
"core.ts",
1718
"fields.ts",

package.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
{
22
"name": "@rotu/structview",
3-
"version": "0.16.1",
3+
"version": "0.16.2",
44
"description": "Read and write structured binary data with typesafe views",
55
"license": "MIT",
66
"type": "module",
77
"repository": {
88
"type": "git",
99
"url": "https://github.com/rotu/structview"
1010
},
11+
"scripts": {
12+
"test": "vitest run && npm run smoke:npm",
13+
"prepack": "rm -rf dist && tsc -p tsconfig.json",
14+
"smoke:npm": "node scripts/smoke-test-npm-package.mjs"
15+
},
1116
"exports": {
12-
".": "./mod.ts",
13-
"./bigendian": "./bigendian.ts",
17+
".": {
18+
"node": "./dist/mod.js",
19+
"default": "./mod.ts"
20+
},
21+
"./bigendian": {
22+
"node": "./dist/bigendian.js",
23+
"default": "./bigendian.ts"
24+
},
1425
"./package.json": "./package.json"
1526
},
16-
"types": "./mod.ts",
1727
"files": [
28+
"dist",
1829
"bigendian.ts",
1930
"core.ts",
2031
"fields.ts",
@@ -27,6 +38,7 @@
2738
"node": ">=22"
2839
},
2940
"devDependencies": {
41+
"typescript": "^5.9.3",
3042
"vitest": "^4.1.2"
3143
}
3244
}

scripts/smoke-test-npm-package.mjs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @file Verifies the packed npm artifact after installation from node_modules.
3+
* Source-level tests cover workspace modules directly, but this catches export,
4+
* prepack, and package-contents regressions that only appear in the published layout.
5+
*/
6+
7+
import { execFile } from "node:child_process"
8+
import { mkdir, rm } from "node:fs/promises"
9+
import { tmpdir } from "node:os"
10+
import { dirname, join, resolve } from "node:path"
11+
import process from "node:process"
12+
import { fileURLToPath } from "node:url"
13+
import { promisify } from "node:util"
14+
15+
const execFileAsync = promisify(execFile)
16+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
17+
const workspaceDir = await mkdirTemp()
18+
const installPrefix = join(workspaceDir, "project")
19+
const npmCacheDir = join(workspaceDir, "npm-cache")
20+
21+
try {
22+
await mkdir(installPrefix, { recursive: true })
23+
await mkdir(npmCacheDir, { recursive: true })
24+
const tarballPath = await packTarball(workspaceDir)
25+
await installTarball(installPrefix, tarballPath, npmCacheDir)
26+
await runSmokeCheck(installPrefix)
27+
} finally {
28+
await rm(workspaceDir, { recursive: true, force: true })
29+
}
30+
31+
async function mkdirTemp() {
32+
const { mkdtemp } = await import("node:fs/promises")
33+
return mkdtemp(join(tmpdir(), "structview-npm-smoke-"))
34+
}
35+
36+
async function packTarball(packDestination) {
37+
const { stdout } = await execFileAsync(
38+
"npm",
39+
["pack", "--quiet", "--pack-destination", packDestination],
40+
{
41+
cwd: repoRoot,
42+
env: process.env,
43+
},
44+
)
45+
const tarballName = stdout.trim().split(/\r?\n/).at(-1)
46+
47+
if (!tarballName) {
48+
throw new Error("npm pack did not report a tarball name")
49+
}
50+
51+
return join(packDestination, tarballName)
52+
}
53+
54+
async function installTarball(installPath, tarballPath, cachePath) {
55+
await execFileAsync(
56+
"npm",
57+
[
58+
"install",
59+
"--ignore-scripts",
60+
"--no-package-lock",
61+
"--prefix",
62+
installPath,
63+
tarballPath,
64+
],
65+
{
66+
cwd: repoRoot,
67+
env: {
68+
...process.env,
69+
npm_config_cache: cachePath,
70+
},
71+
},
72+
)
73+
}
74+
75+
async function runSmokeCheck(installPath) {
76+
await execFileAsync(
77+
process.execPath,
78+
[
79+
"--input-type=module",
80+
"-e",
81+
"import { defineStruct, u8 } from '@rotu/structview'; import { u16be } from '@rotu/structview/bigendian'; class Sample extends defineStruct({ value: u8(0), wide: u16be(1) }) {} const sample = new Sample(new Uint8Array(3)); sample.value = 7; sample.wide = 0x0102; if (sample.value !== 7 || sample.wide !== 0x0102) throw new Error('npm smoke test failed');",
82+
],
83+
{
84+
cwd: installPath,
85+
env: process.env,
86+
},
87+
)
88+
}

scripts/sync-package-metadata.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ import process from "node:process"
33
import denoJsonData from "../deno.json" with { type: "json" }
44
import packageJsonData from "../package.json" with { type: "json" }
55

6-
type PackageExportMap = Record<string, string>
6+
interface PackageConditionalExport {
7+
deno?: string
8+
default?: string
9+
[key: string]: unknown
10+
}
11+
12+
type PackageExportTarget = string | PackageConditionalExport
13+
type PackageExportMap = Record<string, PackageExportTarget>
14+
type DenoExportMap = Record<string, string>
715

816
interface PackageJson {
917
name?: string
@@ -17,7 +25,7 @@ interface DenoJson {
1725
name?: string
1826
version?: string
1927
license?: string
20-
exports?: PackageExportMap
28+
exports?: DenoExportMap
2129
publish?: {
2230
include?: string[]
2331
exclude?: string[]
@@ -79,18 +87,30 @@ function requireString(value: string | undefined, label: string): string {
7987

8088
function deriveDenoExports(
8189
packageExports: PackageExportMap | undefined,
82-
): PackageExportMap {
90+
): DenoExportMap {
8391
if (!packageExports) {
8492
throw new Error("package.json must define an exports object")
8593
}
8694

8795
const denoExports = Object.fromEntries(
88-
Object.entries(packageExports).filter(([subpath, target]) => {
89-
if (typeof target !== "string") {
90-
throw new Error(`Unsupported package export target for ${subpath}`)
96+
Object.entries(packageExports).filter(([subpath]) => {
97+
return subpath !== "./package.json"
98+
}).map(([subpath, target]) => {
99+
if (typeof target === "string") {
100+
return [subpath, target]
91101
}
92102

93-
return subpath !== "./package.json"
103+
if (typeof target.deno === "string") {
104+
return [subpath, target.deno]
105+
}
106+
107+
if (typeof target.default !== "string") {
108+
throw new Error(
109+
`package.json export ${subpath} must provide a string deno or default target`,
110+
)
111+
}
112+
113+
return [subpath, target.default]
94114
}),
95115
)
96116

tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"lib": ["ESNext", "DOM"],
7+
"outDir": "dist",
8+
"verbatimModuleSyntax": true,
9+
"rewriteRelativeImportExtensions": true,
10+
"strict": true,
11+
"skipLibCheck": true
12+
},
13+
"include": [
14+
"bigendian.ts",
15+
"core.ts",
16+
"fields.ts",
17+
"mod.ts",
18+
"types.ts"
19+
]
20+
}

0 commit comments

Comments
 (0)