feat: Support loading the emitted packages with native Node ESM - #199
Draft
TrevorBurnham wants to merge 2 commits into
Draft
feat: Support loading the emitted packages with native Node ESM#199TrevorBurnham wants to merge 2 commits into
TrevorBurnham wants to merge 2 commits into
Conversation
… Node Each emitted `*.css.js` starts with `import './styles.scoped.css'`, and Node has no loader for `.css`. Every module that reaches a class-name map is therefore unloadable under native Node ESM, including Vite SSR in its default configuration, where dependencies are externalized and handed to Node. `stylesheetImport: 'subpath'` emits `import '#stylesheet/<path>'` instead. Node resolves subpath imports through the consuming package's own `imports` map, which, unlike `exports`, also applies to package-internal imports. The map sends bundlers to the stylesheet and Node to an empty module, so the class-name map loads while the stylesheet is still bundled everywhere else. Off by default, since the emitted specifier only resolves once the consuming package merges `getStylesheetPackageImports()` into its manifest. The condition order is load-bearing: every bundler applies `module`, including when targeting Node, so `module` and `browser` come first. Keying on `node` first instead yields a green build carrying the class names but no stylesheet at all under webpack `target: 'node'`, `vite build --ssr` with `ssr.noExternal`, `esbuild --platform=node`, and Rollup with `exportConditions: ['node']`. Measured on the built components package (229 class-name modules): native Node goes from 221 to 324 of 326 export subpaths loadable; webpack's CSS output is byte-identical for both `web` and `node` targets, as is `vite build --ssr` under `ssr.noExternal`. Across 5737 emitted files the only change is one line in each of the 229 modules.
theming-runtime emits ESM, and Node's ESM resolver neither probes extensions nor falls back to a directory index, so importing the package fails before it runs: ERR_UNSUPPORTED_DIR_IMPORT: Directory import '.../shared/theme' is not supported resolving ES modules imported from '.../browser/index.js' This is the whole of the remaining `./theming` failure in the components package. TypeScript passes relative specifiers through to the emitted JavaScript verbatim, so the fix belongs in source. Applied with the require-emitted-extensions autofix over src/browser and src/shared, which is what compiles into lib/browser: 94 specifiers across 24 files, nothing unresolvable or ambiguous, and a second pass changes nothing. src/build is left alone, since it compiles to CommonJS where extensionless resolution works. Emitted lib/browser goes from 60 extensionless relative specifiers to 0, and importing browser/index.js under native Node now succeeds. 341 build tests and 218 browser tests pass; eslint is unchanged at 0 errors and 55 warnings. Node still reparses the package as ESM, because the generated manifest has no "type": "module". Adding that is a separate decision: it drops the reparse warning and removes the dependency on module-syntax detection, but returns ERR_REQUIRE_ESM to any require() consumer on Node below 22.12.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two changes, both aimed at Node's ESM resolver, which neither probes extensions, falls back to a directory index, nor has a loader for
.css.1.
theming-runtimerelative specifierstheming-runtimeemits ESM, so importing the published package fails before it runs:Applied with an autofix over
src/browserandsrc/shared, which is what compiles intolib/browser: 94 specifiers across 24 files, nothing unresolvable or ambiguous, and a second pass changes nothing.src/buildis untouched, since it compiles to CommonJS where extensionless resolution works.Emitted
lib/browsergoes from 60 extensionless relative specifiers to 0, and importingbrowser/index.jsunder native Node now succeeds. This is the whole of the remaining./themingfailure in the components package.2. Opt-in subpath stylesheet imports
The emitted class-name modules carry
import './styles.scoped.css'. Node has no loader for.css, so every module that reaches a class-name map is unloadable, including the package's own entry point. That is what blocks Vite SSR in its default configuration, where dependencies are externalized and handed to Node rather than bundled.exportsconditions cannot help: they do not apply to package-internal relative imports. Theimportsfield does, and it supports conditions. WithstylesheetImport: 'subpath'the build emitsimport '#stylesheet/<path>', andgetStylesheetPackageImports()returns the map the consuming package merges into its manifest. The default stays'relative', so existing behaviour is unchanged.The condition order is the whole design
Conditions match in declaration order. Keying
nodefirst looks right and is actively dangerous: every bundler appliesmoduleeven when it targets Node, so webpack withtarget: 'node',vite build --ssrunderssr.noExternal,esbuild --platform=nodeand Rollup withexportConditions: ['node']all resolve to the empty module and produce a green build carrying the class names and no stylesheet at all.defaultpoints at the real stylesheet rather than the stub, so a resolver applying none of these conditions fails loudly on the.cssextension instead of quietly dropping every style.Evidence
Built the components package with the option on, and diffed against the same build with it off:
package.jsondiffers only byimports. One new file. No class name, CSS byte, orstyles.selectors.jschanged.ERR_UNKNOWN_FILE_EXTENSIONeliminated.webandnodebyte-identical (436,116 B CSS, 597awsui_);vite build --ssrwithssr.noExternalbyte-identical (368,125 B, 556awsui_); Vite client byte-identical including content hash.vite build --ssrexternalized and run under Node goes fromERR_UNKNOWN_FILE_EXTENSIONto rendering 1396 B with 24awsui_classes.Two caveats, both documented on
getStylesheetPackageImports: Vitest appliesnodebut neithermodulenorbrowser, so it resolves to the empty module, which only surfaces undercss: true; and webpack 4 does not implementimportsat all, failing loudly, recoverable withresolve.alias.Jest is fine either way, measured on Jest 29: jsdom applies
browserand gets the real stylesheet, Node env appliesnodeand gets the stub.Follow-ups, not in this PR
theming-runtimemanifest has no"type": "module", so Node reparses it as ESM and warns about the cost. Adding it removes the dependency on module-syntax detection, but returnsERR_REQUIRE_ESMto anyrequire()consumer on Node below 22.12. Worth doing as its own change.