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
48 changes: 48 additions & 0 deletions .changeset/mini-lynx-native-dist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
"@amritk/mini-lynx-native": patch
---

Republish the bridge with the `dist/` its manifest has always promised, and
remove the `development` export condition that hid the miss.

`@amritk/mini-lynx-native@0.2.0` went to npm as a src-only tarball. It was
published by hand rather than through the release workflow, so none of the
things that workflow does before `changeset publish` ever ran: no `bun run
build`, so there was no `dist/`; no `strip-development-exports`, so the
`development` condition survived; no `copy-license`, so the tarball carried no
LICENSE. The manifest still declared `./dist/index.js`, `./dist/background/index.js`
and `./dist/testing/index.js`, and `files` still listed `dist` — every one of
those pointed at nothing.

The surviving condition is why this was survivable rather than fatal, and why it
went unnoticed for a release: it resolved to `./src/*.ts`, and `src` does ship,
so anything honouring it got raw TypeScript and appeared to work. Anything that
did not — plain Node, a bundler on default conditions, `tsc` reading `types` —
got a resolution failure against a package whose exports named files that were
not in the tarball.

0.2.1 is the same code, published through the release workflow, so it carries
`dist/` and its type declarations. Consumers working around the miss by forcing
the `development` condition — a `customConditions` entry in `tsconfig.json`, a
resolve condition in the bundler config, a `--conditions development` flag on
the test command — can drop all three and resolve normally.

The four `@amritk/lynx-*` packages pin the bridge at an exact version, so their
0.2.0 releases still point at the broken tarball; they go out alongside this one
re-pinned to 0.2.1.

**The condition is gone from every package.** It existed so the workspace could
resolve its own packages to source without a build, but it lived in the one
place that ships — the `exports` map — which is what let it reach a tarball at
all. Nothing strips it at publish time now because nothing declares it: the
packages resolve each other through `types`/`import` like any consumer, and the
build simply runs before the type check. Tests are unaffected, having always
used `vitest.config.ts`'s `src` aliases rather than the condition. The
playgrounds and the bundle-size bench keep resolving to source through
repo-local mechanisms that cannot ship.

Publishing out of band can no longer do this quietly. Every publishable package
runs `scripts/check-publishable.mjs` as `prepublishOnly`, which fails the
publish when an exports map points at a `dist/` file that is not on disk, when a
`development` condition is present at all, or when the package directory has no
LICENSE.
24 changes: 12 additions & 12 deletions .claude/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,10 @@ reactivity, no platform.**
`URLSearchParams`, which is a web global and not an ECMAScript one.
- **Depends on:** nothing. `@amritk/runtime-validators` is an optional peer of
`/schema` alone.
- **Build:** the same `tsgo` + `tsc-alias` + `strip-comments` pipeline. Both
dependents resolve it through the `development` condition while type-checking
(`customConditions` in their `tsconfig.json`, dropped again in
`tsconfig.build.json`) so CI can type-check before it builds; the emit
resolves it through `types` instead, which is why `bun run --workspaces build`
builds this package first.
- **Build:** the same `tsgo` + `tsc-alias` + `strip-comments` pipeline. Its
dependents resolve it through `types`/`import` like any consumer, in the type
check as well as the emit, which is why `bun run --workspaces build` builds
this package first and why CI builds before it type-checks.

### `@amritk/mini-lynx-native` (`packages/mini-lynx-native`)

Expand Down Expand Up @@ -408,10 +406,12 @@ checked.

Two conventions keep them honest, and both are worth preserving:

- **They resolve the packages through the `development` condition**, pinned in
each app's `vite.config.ts` and `tsconfig.json`, so they build from `src` and
run in a fresh clone with no prior `bun run build`. Packaging is deliberately
not their job — `scripts/consumer-e2e.test.ts` packs and installs real
- **They resolve the packages the way a consumer does**, through `types` and
`import` to `dist`, so the packages have to be built before either app is
type-checked or built. They used to pin a `development` condition and build
straight from `src`; that condition shipped in the published manifests and
pointed at the `src` the tarballs also carry, so it is gone. Packaging is
still not their job — `scripts/consumer-e2e.test.ts` packs and installs real
tarballs for that.
- **The root `build`, `types:check` and `test` include them**, so a breaking
change to a package fails CI in the playground too. `playground-mini-lynx`
Expand Down Expand Up @@ -475,8 +475,8 @@ reason.
`bun run build`. It loads every compiled module under plain Node, drives the
built `mini-lynx` runtime through its memory host, and — in
`consumer-e2e.test.ts` — packs both packages the way `release:publish` does
(`catalog:`/`workspace:` resolved, the `development` condition stripped),
installs the tarballs into scratch projects, and imports every declared
(`catalog:`/`workspace:` resolved), installs the tarballs into scratch
projects, and imports every declared
subpath from them. That catches build-, pack- and manifest-level breakage the
src-aliased suite cannot see by construction.

Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,24 @@ jobs:
bun run generate-llms
git diff --exit-code llms.txt llms-full.txt

# Ahead of the type check, not after it. The packages used to resolve each
# other's SOURCE while type-checking, through a `development` condition in
# their exports maps — which meant the manifests shipped a condition
# pointing at `./src/*.ts` inside a tarball that also ships `src`, so
# anything honouring it handed consumers raw TypeScript. The condition is
# gone; cross-package imports resolve to `dist` like a consumer's do, and
# `dist` has to exist by the time anything reads it.
- name: Build
run: bun run build

- name: Type check
run: bun run types:check

# Unaffected by the ordering above: vitest.config.ts aliases every
# workspace package to its `src/`, so the suite has never needed a build.
- name: Test
run: bun run test

- name: Build
run: bun run build

# Loads every compiled module under plain Node, drives the built
# mini-lynx runtime end-to-end through its fake Element PAPI, and
# installs packed tarballs like an npm consumer — catches build-, pack-
Expand Down
17 changes: 13 additions & 4 deletions .github/workflows/deploy-playgrounds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,19 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

# The apps resolve `@amritk/mini` and `@amritk/mini-lynx` through the
# `development` condition, so they build straight from `src` and need no
# prior package build. Type-checking them is still worth the seconds: a
# broken playground should fail here rather than deploy.
# The apps used to resolve `@amritk/mini` and `@amritk/mini-lynx` through
# a `development` condition and build straight from `src`. That condition
# is gone — it shipped in the published manifests and pointed at the `src`
# the tarballs also carry — so the playgrounds now resolve `dist` like any
# consumer, and the packages have to be built before either app is
# type-checked or built. Building the whole workspace rather than the
# app's dependency closure keeps this from needing an edit every time a
# playground picks up another package.
- name: Build packages
run: bun run --filter './packages/*' build

# Worth the seconds: a broken playground should fail here rather than
# deploy.
- name: Type check
run: bun run --filter "@amritk/${{ matrix.app }}" types:check

Expand Down
7 changes: 1 addition & 6 deletions apps/playground-mini-lynx/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,7 @@
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"jsx": "react-jsx",
"jsxImportSource": "@amritk/mini-lynx",
// Resolve `@amritk/mini-lynx` and every subpath to the workspace's SOURCE
// through the `development` condition the package already declares. The
// same condition is set in `vite.config.ts`, so the playground runs and
// builds in a fresh clone with no prior `bun run build`.
"customConditions": ["development"]
"jsxImportSource": "@amritk/mini-lynx"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
}
5 changes: 0 additions & 5 deletions apps/playground-mini-lynx/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import { defineConfig } from 'vite'

export default defineConfig({
resolve: {
// Pin the `development` condition in both `serve` and `build`, so
// `@amritk/mini-lynx` and its subpaths resolve to source rather than to
// `dist/`. The playground then always exercises the code in this checkout
// and never depends on the packages having been built first.
conditions: ['development', 'module', 'browser'],
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
},
build: {
Expand Down
27 changes: 16 additions & 11 deletions apps/playground-mini/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,22 @@ that must not scroll the demo out from under you.

## How it resolves the package

`vite.config.ts` and `tsconfig.json` both pin the `development` condition, so
`@amritk/mini` and every subpath resolve to `packages/mini/src` rather than to
`dist/`. That means the playground runs in a fresh clone with no prior build,
always exercises the code in this checkout, and needs no `paths` table kept in
sync with the exports map.

The one thing that cannot resolve that way is `@amritk/mini/vite` — Vite loads
its own config with plain Node resolution — so `vite.config.ts` imports the two
plugins by relative path. Both are on: `catchCalledSignals` fails the build on
`attr={signal()}`, and `acceptHotUpdates` makes `src/main.tsx` the hot-update
boundary.
`@amritk/mini` and every subpath resolve through the package's `exports` map to
`packages/mini/dist`, exactly as they would for anyone installing from npm. So
the playground needs `bun run build` at the workspace root before it will start.

It used to pin a `development` condition in both `vite.config.ts` and
`tsconfig.json` and resolve to `src` instead, which meant no build was needed.
That condition lived in the packages' own `exports` maps, which is what made it
reachable from a published tarball — and `@amritk/mini-lynx-native@0.2.0`
shipped one, resolving consumers to raw TypeScript. Needing a build first is the
price of the condition not existing at all.

`@amritk/mini/vite` is the exception, and it is a bootstrapping one: a config
file has to load before Vite can do anything, including before the first build
exists, so `vite.config.ts` imports the two plugins by relative path instead.
Both are on: `catchCalledSignals` fails the build on `attr={signal()}`, and
`acceptHotUpdates` makes `src/main.tsx` the hot-update boundary.

Packaging is deliberately *not* what this app checks. `scripts/consumer-e2e.test.ts`
packs and installs real tarballs for that, which is the honest test of it.
Expand Down
8 changes: 1 addition & 7 deletions apps/playground-mini/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"jsx": "react-jsx",
"jsxImportSource": "@amritk/mini",
// Resolve `@amritk/mini` and every subpath to the workspace's SOURCE rather
// than to `dist`, through the `development` condition the package already
// declares. The same condition is set in `vite.config.ts`, and it is what
// lets the playground run (and build) in a fresh clone with no prior
// `bun run build` — no `paths` table to keep in sync with the exports map.
"customConditions": ["development"]
"jsxImportSource": "@amritk/mini"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
}
18 changes: 5 additions & 13 deletions apps/playground-mini/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { defineConfig } from 'vite'

// The plugins are imported from the workspace SOURCE rather than through
// `@amritk/mini/vite`. Vite loads this config with plain Node resolution, which
// does not apply the `development` condition the rest of the app resolves
// through — so the package name here would point at `dist/` and fail before the
// package has ever been built. A relative import keeps `bun run dev` working in
// a fresh clone, and has the playground dogfood the plugin's source.
// `@amritk/mini/vite`, because a config file has to load before anything else
// can: the package name would point at a `dist/` that a fresh clone has not
// built yet, and the failure would be vite refusing to start rather than
// anything about the app. A relative import also has the playground dogfood the
// plugin's source.
import { acceptHotUpdates, catchCalledSignals } from '../../packages/mini/src/vite/index.ts'

export default defineConfig({
Expand All @@ -18,14 +18,6 @@ export default defineConfig({
// required: see `src/main.tsx`.
acceptHotUpdates(),
],
resolve: {
// Pin the `development` condition in both `serve` and `build`. Vite's
// default is `development|production`, which would resolve `@amritk/mini`
// to `dist/` for a production build and make the playground depend on
// build ordering; resolving to source instead means this app always
// exercises the code in this checkout, which is the point of a playground.
conditions: ['development', 'module', 'browser'],
},
build: {
target: 'es2022',
// The whole app is one page of demos — a single chunk keeps the Cloudflare
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"bench:reconciler": "bun run scripts/bench-reconciler.ts",
"generate-llms": "bun run scripts/generate-llms.ts",
"release:version": "changeset version && bun install --no-frozen-lockfile",
"release:publish": "bun run scripts/resolve-workspace-protocol.ts && bun run scripts/strip-development-exports.ts && bun run scripts/copy-license.ts && changeset publish"
"release:publish": "bun run scripts/resolve-workspace-protocol.ts && bun run scripts/copy-license.ts && changeset publish"
},
"devDependencies": {
"@biomejs/biome": "2.4.4",
Expand Down
3 changes: 1 addition & 2 deletions packages/lynx-deep-linking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,19 @@
},
"scripts": {
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs",
"prepublishOnly": "node ../../scripts/check-publishable.mjs",
"types:check": "tsgo -p . --noEmit",
"test": "NODE_ENV=production vitest run --root ../.. packages/lynx-deep-linking/"
},
"exports": {
"./package.json": "./package.json",
"./lynx.lib.json": "./lynx.lib.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./testing": {
"development": "./src/testing/index.ts",
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing/index.js",
"default": "./dist/testing/index.js"
Expand Down
8 changes: 1 addition & 7 deletions packages/lynx-deep-linking/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,7 @@
// Re-asserted because the shared `../../tsconfig.build.json` (applied last
// in `extends`) resets `lib`/`types` back to the Node-only defaults.
"lib": ["ESNext"],
"types": [],
// Dropped for the emit pass, exactly as `@amritk/lynx-location` drops it:
// here the `development` condition would put another package's `src` inside
// this one's `rootDir`. The build resolves both dependencies through the
// normal `types` condition instead, which is why the root `build` script
// builds them first.
"customConditions": []
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules", "**/*.test.ts"]
Expand Down
8 changes: 1 addition & 7 deletions packages/lynx-deep-linking/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,7 @@
// browser or a Node process. It is also what keeps `parse-url.ts` off the
// `URL` global, which is a web platform API a native engine need not have.
"lib": ["ESNext"],
"types": [],
// Resolve `@amritk/mini-lynx-native` and `@amritk/mini-helpers` to the
// workspace's SOURCE through the `development` condition they already
// declare, rather than to their `dist`. CI type-checks BEFORE it builds, so
// without this a fresh clone would look for a `.d.ts` that does not exist
// yet.
"customConditions": ["development"]
"types": []
},
"include": ["src/**/*.ts"]
}
3 changes: 1 addition & 2 deletions packages/lynx-dialogs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,19 @@
},
"scripts": {
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs",
"prepublishOnly": "node ../../scripts/check-publishable.mjs",
"types:check": "tsgo -p . --noEmit",
"test": "NODE_ENV=production vitest run --root ../.. packages/lynx-dialogs/"
},
"exports": {
"./package.json": "./package.json",
"./lynx.lib.json": "./lynx.lib.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./testing": {
"development": "./src/testing/index.ts",
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing/index.js",
"default": "./dist/testing/index.js"
Expand Down
7 changes: 1 addition & 6 deletions packages/lynx-dialogs/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@
// Re-asserted because the shared `../../tsconfig.build.json` (applied last
// in `extends`) resets `lib`/`types` back to the Node-only defaults.
"lib": ["ESNext"],
"types": [],
// Dropped for the emit pass, exactly as `@amritk/mini-lynx` drops it: here
// the `development` condition would put another package's `src` inside this
// one's `rootDir`. The build resolves the bridge through the normal `types`
// condition instead, which is why the root `build` script builds it first.
"customConditions": []
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules", "**/*.test.ts"]
Expand Down
7 changes: 1 addition & 6 deletions packages/lynx-dialogs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
// them: this is Lynx code, and neither of Lynx's two script contexts is a
// browser or a Node process.
"lib": ["ESNext"],
"types": [],
// Resolve `@amritk/mini-lynx-native` to the workspace's SOURCE through the
// `development` condition it already declares, rather than to its `dist`.
// CI type-checks BEFORE it builds, so without this a fresh clone would look
// for a `.d.ts` that does not exist yet.
"customConditions": ["development"]
"types": []
},
"include": ["src/**/*.ts"]
}
3 changes: 1 addition & 2 deletions packages/lynx-location/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,19 @@
},
"scripts": {
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs",
"prepublishOnly": "node ../../scripts/check-publishable.mjs",
"types:check": "tsgo -p . --noEmit",
"test": "NODE_ENV=production vitest run --root ../.. packages/lynx-location/"
},
"exports": {
"./package.json": "./package.json",
"./lynx.lib.json": "./lynx.lib.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./testing": {
"development": "./src/testing/index.ts",
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing/index.js",
"default": "./dist/testing/index.js"
Expand Down
7 changes: 1 addition & 6 deletions packages/lynx-location/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@
// Re-asserted because the shared `../../tsconfig.build.json` (applied last
// in `extends`) resets `lib`/`types` back to the Node-only defaults.
"lib": ["ESNext"],
"types": [],
// Dropped for the emit pass, exactly as `@amritk/mini-lynx` drops it: here
// the `development` condition would put another package's `src` inside this
// one's `rootDir`. The build resolves the bridge through the normal `types`
// condition instead, which is why the root `build` script builds it first.
"customConditions": []
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules", "**/*.test.ts"]
Expand Down
Loading
Loading