Skip to content

Commit e180910

Browse files
committed
fix(astro): drop named Image re-export — broke config load in vanilla Node
Closes kychee-com/run402-private#400. v0.1.2 added 'export { default as Image } from "./Image.astro"' to dist/index.js so 'import { Image } from "@run402/astro"' would work. That broke the entire package: the Astro CLI loads astro.config.mjs via vanilla Node before Vite is alive, and Node has no loader for .astro extensions. The user's config does 'import { run402 } from "@run402/astro"', Node evaluates dist/index.js top-to-bottom, hits the .astro re-export, and dies with 'Unknown file extension.' vite.ssr.noExternal can't fix this because it's a Vite-pipeline setting; Vite hasn't started yet at config load time. Chicken-and-egg. Fix: drop the re-export entirely. The integration entry point ('@run402/astro') stays pure-JS so vanilla Node can evaluate it during config load. The component is reached only via the subpath form 'import Image from "@run402/astro/Image.astro"', which Vite's plugin pipeline resolves once it's alive. Default-import syntax because .astro files have exactly one default export. README and the entry-point JSDoc updated to document the subpath as the ONLY correct import form. The 'ergonomic named import' is documented as an anti-pattern with a pointer to the config-load constraint. Smoke test in CI: removed the grep guard for .astro substrings (false-positives on JSDoc / comment text) and kept the runtime 'node -e import()' check, which is the authoritative test for config-load safety.
1 parent 2b9d852 commit e180910

4 files changed

Lines changed: 57 additions & 49 deletions

File tree

.github/workflows/publish-astro.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,16 @@ jobs:
167167
# broken re-export specifier) without requiring an Astro build.
168168
grep -q "^export function run402" "$SMOKE/extract/package/dist/index.js" \
169169
|| (echo "Missing run402 export from dist/index.js" && exit 1)
170-
grep -q "^export { default as Image } from" "$SMOKE/extract/package/dist/index.js" \
171-
|| (echo "Missing Image re-export from dist/index.js" && exit 1)
170+
# Critical invariant: vanilla Node MUST be able to evaluate
171+
# dist/index.js without an Astro/Vite resolver. The Astro CLI
172+
# loads astro.config.mjs via Node BEFORE Vite is alive — any
173+
# top-level .astro reference from this entry point dies with
174+
# "Unknown file extension." kychee-com/run402-private#400 is
175+
# the regression this check now prevents. (We don't grep for
176+
# ".astro" in the source because JSDoc / comment text often
177+
# mentions it legitimately; the actual runtime import is the
178+
# only authoritative test.)
179+
node -e "import('$SMOKE/extract/package/dist/index.js').then(m => { if (typeof m.run402 !== 'function') { console.error('run402 export is not a function'); process.exit(1); } console.log('Smoke OK'); }).catch(e => { console.error('Import failed:', e.message); process.exit(1); })"
172180
grep -q "buildPictureHtml" "$SMOKE/extract/package/dist/Image.astro" \
173181
|| (echo "Image.astro missing expected import" && exit 1)
174182

astro/README.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ In CI, the integration auto-detects GitHub OIDC credentials when the workflow ha
3636

3737
```astro
3838
---
39-
import { Image } from '@run402/astro';
39+
import Image from '@run402/astro/Image.astro';
4040
---
4141
<Image src="./images/hero.jpg" alt="Sunset over the Pacific" sizes="100vw" priority />
4242
@@ -45,15 +45,7 @@ import { Image } from '@run402/astro';
4545

4646
`src` is resolved relative to the importing `.astro` file. TypeScript path aliases (`@/*`) also work if you have them in `tsconfig.json`.
4747

48-
**Alternative subpath import** also works if you prefer being explicit about the `.astro` component file:
49-
50-
```astro
51-
---
52-
import Image from '@run402/astro/Image.astro';
53-
---
54-
```
55-
56-
(Note: subpath form uses default-import syntax because `.astro` files only have a default export. Both forms resolve to the same component.)
48+
**Note on the import shape.** `.astro` components have a single default export, so `import Image from '@run402/astro/Image.astro'` (default-import, subpath) is the only correct form. There is no `import { Image } from '@run402/astro'` named export — anything imported from `@run402/astro` must evaluate cleanly under vanilla Node so it can be loaded from `astro.config.mjs` before Vite is alive, and a top-level re-export of an `.astro` module breaks that boundary.
5749

5850
## Generated HTML
5951

astro/src/astro-modules.d.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

astro/src/index.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
/**
22
* `@run402/astro` — Astro integration for Run402 image variants.
33
*
4-
* Exports:
4+
* Exports from this module (pure JS, safe to import from astro.config.mjs):
55
* - `run402(options?)` — the Astro integration factory. Add to
66
* `astro.config.mjs` `integrations: [run402()]`.
7-
* - `Image` — re-export of the `.astro` component file. Users import it
8-
* via `import { Image } from '@run402/astro'` and use it directly in
9-
* templates.
107
* - Types: `Run402AstroOptions`, `ImageProps`, `AssetRef`, `AssetVariant`.
118
*
12-
* The integration is intentionally small. The real work lives in the Vite
13-
* plugin (image discovery, upload, source rewriting, public/ exclusion);
14-
* this module just wires the plugin into Astro's lifecycle and validates
15-
* configuration up front.
9+
* The `<Image>` Astro component is shipped as a separate subpath:
10+
*
11+
* import Image from '@run402/astro/Image.astro';
12+
*
13+
* Why subpath: this entry point must evaluate cleanly under vanilla
14+
* Node (Astro CLI loads `astro.config.mjs` BEFORE Vite is alive, so
15+
* any top-level `.astro` reference from here dies with "Unknown file
16+
* extension"). The component file is reached only by Vite/Astro's
17+
* plugin pipeline once Vite is running, which knows how to compile
18+
* `.astro` source.
19+
*
20+
* The integration itself is intentionally small. The real work lives
21+
* in the Vite plugin (image discovery, upload, source rewriting,
22+
* public/ exclusion); this module just wires the plugin into Astro's
23+
* lifecycle and validates configuration up front.
1624
*/
1725

1826
import { BuildCache } from "./cache.js";
@@ -143,21 +151,37 @@ function configRootToPath(root: URL | string | undefined): string {
143151
return String(root);
144152
}
145153

146-
// Named re-export of the <Image> component so consumers can either:
147-
// - import { Image } from '@run402/astro' — ergonomic, matches React/Next conventions
148-
// - import Image from '@run402/astro/Image.astro' — explicit subpath form
154+
// <Image> is intentionally NOT re-exported from this module.
155+
//
156+
// We tried in v0.1.2 (kychee-com/run402-private#399):
157+
//
158+
// export { default as Image } from "./Image.astro";
159+
//
160+
// That broke the entire package (kychee-com/run402-private#400). The
161+
// Astro CLI loads `astro.config.mjs` via Node's ESM loader BEFORE Vite
162+
// is alive. The user's config does `import { run402 } from
163+
// '@run402/astro'`. Node evaluates this module top-to-bottom, hits the
164+
// `export ... from "./Image.astro"` statement, has no loader for the
165+
// `.astro` extension, and dies. Vite's `noExternal` / Astro's compiler
166+
// plugin can't help because they aren't reachable yet — the config
167+
// itself hasn't loaded.
168+
//
169+
// The correct boundary: the integration entry point (this file) must
170+
// stay pure-JS so Node can evaluate it at config-load time. The Astro
171+
// component is reached only after Vite is up, via the subpath import:
172+
//
173+
// import Image from '@run402/astro/Image.astro';
149174
//
150-
// Both forms resolve to the SAME `.astro` component shipped under
151-
// `dist/Image.astro`. The subpath form was the only thing that worked
152-
// in v0.1.1 (and was documented inconsistently — see kychee-com/
153-
// run402-private#399). v0.1.2 makes both forms work and aligns docs to
154-
// the named form.
175+
// That subpath is declared in the package's `exports` map and resolved
176+
// by Vite/Astro's plugin pipeline. `.astro` files have a single default
177+
// export, so the `import Image` (default) form is the only correct
178+
// shape regardless.
155179
//
156-
// The tsc-side `*.astro` ambient declaration in `astro-modules.d.ts`
157-
// lets this re-export type-check; at runtime Vite/Astro resolves the
158-
// `./Image.astro` specifier against `dist/Image.astro` per the
159-
// package's exports map.
160-
export { default as Image } from "./Image.astro";
180+
// If a future v0.2 pivots to the import-based pattern
181+
// (`import hero from './hero.jpg'`), the integration entry point STILL
182+
// stays pure-JS — the Vite plugin claims image imports in `load`,
183+
// which is also after Vite is alive. The config-load constraint
184+
// applies to anything imported from `'@run402/astro'`.
161185

162186
// Type re-exports for consumers.
163187
export type { AssetRef, AssetVariant, ImageProps, Run402AstroOptions } from "./types.js";

0 commit comments

Comments
 (0)