diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 779844d..b2216f2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -33,5 +33,5 @@ yarn example start # the demo app - **`yarn lint` is zero errors and zero warnings.** Not "mostly clean". - **Every view model has a test.** No exceptions worth arguing about. - **The published `lib/` must not contain `@/`.** `yarn prepare` resolves it; if you change - the build, verify with `grep -r '@/' lib/`. + the build, verify with `yarn verify:lib` — it also catches absolute paths. - **Never commit to `main`.** Branch first. diff --git a/.claude/rules/imports.md b/.claude/rules/imports.md index a6edf74..be63832 100644 --- a/.claude/rules/imports.md +++ b/.claude/rules/imports.md @@ -24,8 +24,13 @@ import { CICERONE } from '@/constants'; | `~/` | `example/src/` | the example app | **They must stay distinct.** The example's babel config also transforms library files, so a -shared prefix makes `@/context` in the library resolve to `example/src/context`. Both aliases -are declared with absolute paths for the same reason. +shared prefix makes `@/context` in the library resolve to `example/src/context`. + +**`@` maps to `./src`, never to an absolute path.** `babel-plugin-module-resolver` emits the +alias target verbatim, so `path.resolve(__dirname, 'src')` ships the build machine's own +path — that is how `0.1.0` reached npm with `import ... from "/Users/.../src/constants"` in +22 files. The example is safe either way: it has no `module-resolver`, Metro resolves both +aliases from `example/tsconfig.json`. Three places have to agree, and all three are already wired: @@ -51,7 +56,7 @@ default `false` it ignores `babel.config.js` and ships `import '@/constants'` to After touching anything in the build, verify: ```sh -yarn prepare && grep -r '@/' lib/ && echo "LEAK" || echo "clean" +yarn prepare && yarn verify:lib ``` ## Order diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0076c0..a5ea2a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,9 @@ jobs: - name: Build package run: yarn prepare + - name: Check lib/ is portable + run: yarn verify:lib + build-android: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 730ed00..b691e9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,13 +73,8 @@ jobs: - name: Build package run: yarn prepare - - name: Check the '@/' alias did not reach lib/ - run: | - if grep -rq '@/' lib/; then - echo "::error::The '@/' alias leaked into lib/, consumers cannot resolve it." - grep -rn '@/' lib/ - exit 1 - fi + - name: Check lib/ is portable + run: yarn verify:lib # The very first version goes up by hand, before npm can trust this workflow. - name: Check whether the version is already on npm diff --git a/babel.config.js b/babel.config.js index 988fdae..0e38a92 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,11 +1,9 @@ -const path = require('path'); - module.exports = { plugins: [ [ 'module-resolver', { - alias: { '@': path.resolve(__dirname, 'src') }, + alias: { '@': './src' }, extensions: ['.ts', '.tsx', '.js', '.jsx'], }, ], diff --git a/package.json b/package.json index 198b50b..1db793e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "clean": "del-cli lib", "prepare": "husky || true; bob build && tsc-alias -p tsconfig.build.json --outDir lib/typescript", "typecheck": "tsc", + "verify:lib": "bash scripts/verify-lib.sh", "release": "release-it --only-version", "test": "jest", "lint": "eslint \"**/*.{js,jsx,cjs,mjs,ts,tsx}\"", diff --git a/scripts/verify-lib.sh b/scripts/verify-lib.sh new file mode 100755 index 0000000..fd0fba7 --- /dev/null +++ b/scripts/verify-lib.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Consumers resolve lib/ from their own node_modules, so every specifier in it must be portable. +set -uo pipefail + +if [ ! -d lib ]; then + echo "error: lib/ does not exist, run 'yarn prepare' first" + exit 1 +fi + +status=0 + +if grep -rq '@/' lib/; then + echo "error: the '@/' alias reached lib/, consumers cannot resolve it" + grep -rn '@/' lib/ + status=1 +fi + +# An absolute specifier means module-resolver baked in a path from the build machine. +if grep -rqE "from [\"']/|require\([\"']/" lib/; then + echo "error: lib/ imports from an absolute path" + grep -rnE "from [\"']/|require\([\"']/" lib/ + status=1 +fi + +if [ "$status" -eq 0 ]; then + echo "lib/ is portable" +fi + +exit "$status"