diff --git a/.changepacks/changepack_log_adapter_framework_entries.json b/.changepacks/changepack_log_adapter_framework_entries.json
new file mode 100644
index 000000000..2a623bd0d
--- /dev/null
+++ b/.changepacks/changepack_log_adapter_framework_entries.json
@@ -0,0 +1,7 @@
+{
+ "changes": {
+ "packages/webpack-plugin/package.json": "Patch"
+ },
+ "note": "Avoid scanning excluded framework entry directories when an adapter supplies the complete production extraction inventory, preserving generic entry closure and explicitly included packages. No new build errors.",
+ "date": "2026-10-23T00:00:00.000Z"
+}
diff --git a/.changepacks/changepack_log_guard_normalization_allocations.json b/.changepacks/changepack_log_guard_normalization_allocations.json
new file mode 100644
index 000000000..b0b134211
--- /dev/null
+++ b/.changepacks/changepack_log_guard_normalization_allocations.json
@@ -0,0 +1,7 @@
+{
+ "changes": {
+ "packages/plugin-utils/package.json": "Patch"
+ },
+ "note": "Reduce temporary allocations while preserving compiled-source guard facts, scope analysis and diagnostics. No new build errors.",
+ "date": "2026-10-23T00:00:00.000Z"
+}
diff --git a/.changepacks/changepack_log_imported_source_diagnostics.json b/.changepacks/changepack_log_imported_source_diagnostics.json
new file mode 100644
index 000000000..25137a5c9
--- /dev/null
+++ b/.changepacks/changepack_log_imported_source_diagnostics.json
@@ -0,0 +1,7 @@
+{
+ "changes": {
+ "bindings/devup-ui-wasm/package.json": "Patch"
+ },
+ "note": "Exercise unsupported child grammars and ignored styled imports without changing resolver behavior. Isolate parser diagnostic coordinate mapping and verify real malformed generated source retains generated coordinates without authored edit remapping. No new build errors.",
+ "date": "2026-10-21T00:00:00.000Z"
+}
diff --git a/.changepacks/changepack_log_plugin_core.json b/.changepacks/changepack_log_plugin_core.json
new file mode 100644
index 000000000..5f3dd6221
--- /dev/null
+++ b/.changepacks/changepack_log_plugin_core.json
@@ -0,0 +1,13 @@
+{
+ "changes": {
+ "bindings/devup-ui-wasm/package.json": "Patch",
+ "packages/plugin-utils/package.json": "Patch",
+ "packages/vite-plugin/package.json": "Patch",
+ "packages/webpack-plugin/package.json": "Patch",
+ "packages/rsbuild-plugin/package.json": "Patch",
+ "packages/bun-plugin/package.json": "Patch",
+ "packages/next-plugin/package.json": "Patch"
+ },
+ "note": "Use bundler project roots and active export conditions for build-time module resolution, resolve inherited tsconfig paths and dotted module specifiers, transform modern modules and compiled MDX, match included packages literally, and refresh empty theme declarations. Skip unrequested source maps, surface resolver faults before stylesheet mutation, and report WASM panic locations. Malformed or cyclic configuration, resolver failures, requested graph/prewarm failures and disabled Rsbuild shared splitting now stop builds with context; optional numbering fallback warns about its deterministic impact. Bun defaults to compact names and retains imported build-time dependency watch edges. Webpack and Rsbuild defer package manifest syntax or shape failures until their entries reach the importing file; discovery without entries still fails closed. Rsbuild releases the shared build engine after a failed build, and generated, output and project-root directories are no longer numbered as sources. Bun follows local barrels and prepares only demanded Markdown before replaying detection and extraction, restoring valid MDX imports after the main merge; failed preparation retains its located cause and cannot produce a successful untransformed build.",
+ "date": "2026-10-01T00:00:00.000Z"
+}
diff --git a/.changepacks/changepack_log_plugin_core_ci.json b/.changepacks/changepack_log_plugin_core_ci.json
new file mode 100644
index 000000000..398fa5756
--- /dev/null
+++ b/.changepacks/changepack_log_plugin_core_ci.json
@@ -0,0 +1,10 @@
+{
+ "changes": {
+ "packages/plugin-utils/package.json": "Patch",
+ "packages/vite-plugin/package.json": "Patch",
+ "packages/webpack-plugin/package.json": "Patch",
+ "packages/rsbuild-plugin/package.json": "Patch"
+ },
+ "note": "Keep raw MDX out of static import graphs unless the caller compiles MDX before extraction, preserving Next production prewarm behavior while MDX-aware plugins opt in. Normalize Webpack inline-loader entry resources before filesystem lookup and skip virtual or pitch-only entries without swallowing real I/O failures. Isolate watched-config regression diagnostics while preserving the prior theme on invalid configuration. No new build-error category is introduced.",
+ "date": "2026-10-01T00:00:00.000Z"
+}
diff --git a/.changepacks/changepack_log_plugin_core_mdx.json b/.changepacks/changepack_log_plugin_core_mdx.json
new file mode 100644
index 000000000..da179669f
--- /dev/null
+++ b/.changepacks/changepack_log_plugin_core_mdx.json
@@ -0,0 +1,7 @@
+{
+ "changes": {
+ "bindings/devup-ui-wasm/package.json": "Patch"
+ },
+ "note": "Extract compiled MDX with its real source filename, including JSX-preserving alias imports and exact static computations. Raw or invalid MDX and unsupported extraction extensions now produce located build errors instead of untransformed styling placeholders.",
+ "date": "2026-10-01T00:00:00.000Z"
+}
diff --git a/.changepacks/changepack_log_unprepared_markdown_parity.json b/.changepacks/changepack_log_unprepared_markdown_parity.json
new file mode 100644
index 000000000..eb1087df2
--- /dev/null
+++ b/.changepacks/changepack_log_unprepared_markdown_parity.json
@@ -0,0 +1,11 @@
+{
+ "changes": {
+ "packages/plugin-utils/package.json": "Patch",
+ "packages/next-plugin/package.json": "Patch",
+ "packages/bun-plugin/package.json": "Patch",
+ "packages/vite-plugin/package.json": "Patch",
+ "packages/webpack-plugin/package.json": "Patch"
+ },
+ "note": "Keep unprepared Markdown unreadable during Next and Webpack evaluation as on main, while preserving strict compiler-cache guards for preparation-owning adapters. Use neutral portable test scratch paths. Actual compiled-MDX evaluation remains separate work. No new build errors.",
+ "date": "2026-10-21T00:00:00.000Z"
+}
diff --git a/.changepacks/changepack_log_webpack_entry_resources.json b/.changepacks/changepack_log_webpack_entry_resources.json
new file mode 100644
index 000000000..16f87a025
--- /dev/null
+++ b/.changepacks/changepack_log_webpack_entry_resources.json
@@ -0,0 +1,7 @@
+{
+ "changes": {
+ "packages/webpack-plugin/package.json": "Patch"
+ },
+ "note": "Normalize Webpack entry loader requests before physical class-slot reservation, retaining bare packages and invalid-resource errors. Verify relocated public require/import entries use the installed source/source/CSS loaders and share the consumer WASM identity. No new build errors.",
+ "date": "2026-10-21T00:00:00.000Z"
+}
diff --git a/bindings/devup-ui-wasm/README.md b/bindings/devup-ui-wasm/README.md
index 1032dec5d..3c676c8b0 100644
--- a/bindings/devup-ui-wasm/README.md
+++ b/bindings/devup-ui-wasm/README.md
@@ -1,9 +1,100 @@
# @devup-ui/wasm
-The Devup UI compiler as a Node.js WebAssembly module. It is the engine the
-build plugins (`@devup-ui/vite-plugin`, `next-plugin`, `webpack-plugin`,
-`rsbuild-plugin`, `bun-plugin`) call; most projects never import it directly.
+Build-time CSS extraction bindings used by the Devup UI bundler plugins.
+## Extraction boundaries
+
+`codeExtract` generates a source map. `codeExtractWithoutSourceMap` skips map
+generation, including when a module resolver is installed; its `map` getter is
+`undefined`. Both functions return transformed source and watched dependencies.
+
+`setModuleResolver` accepts a synchronous `(specifier, importer)` callback.
+Return `{ path: string, code: string }` for a resolved module, or
+`{ ignored: true }` for an explicitly ignored native alias. An ignored result
+has no source/path and never reads `path`, `code` or `sourceType` getters.
+`null` and `undefined` mean ordinary unresolved imports. Thrown exceptions, property-getter
+and reflection failures, and missing/non-string fields instead fail extraction:
+
+```text
+/src/App.tsx:1:1: module resolver cannot use `./tokens` at build time: original cause
+```
+
+The importer includes transitive importers. The resolver contract does not
+provide an import span, so this boundary reports `1:1` rather than guessing a
+position from source text. The first resolver fault takes precedence over the
+extractor's unresolved-module fallback and is checked **before** sheet updates.
+The previous stylesheet contribution therefore survives a failed re-extraction.
+Class/file numbering performed inside extraction is not rolled back.
+
+Ignored imports are empty CommonJS modules, not renamed or virtual files. Missing
+named values are `undefined`, defaults follow the existing CommonJS interop, and
+namespace/require values are empty objects. No ignored file becomes a watched
+dependency or is sent through the parser. The `ignored` discriminator must be
+`true` or absent; malformed discriminators retain the same located resolver-fault
+priority and previous-sheet preservation as malformed file results.
+
+## Panic diagnostics
+
+WASM initialization installs this binding's own `std::panic::set_hook`, with no
+additional dependency or feature flag. It reports Rust's panic payload and
+`file:line:column` location to `console.error` before the trap. A failing host
+console does not replace the original panic. This is diagnostic reporting, not
+panic recovery: with `panic=abort`, a panic still traps and the instance should
+be discarded. Normal resolver/configuration failures use fallible JS errors.
+
+Owned binding code does not unwrap user input or explicitly panic. Sheet mutex
+poisoning is recovered, and no resolver callback is called while holding a sheet
+lock or a thread-local resolver borrow. Panics originating in downstream crates
+remain outside this binding's recovery contract.
+
+## Verification
+
+From the repository root:
+
+```sh
+cargo test -p devup-ui-wasm boundary_ -- --nocapture
+```
+
+After rebuilding `pkg` with this package's `bun run build`, run from the
+repository root:
+
+```sh
+node bindings/devup-ui-wasm/boundary-regressions.mjs
+node bindings/devup-ui-wasm/ignored-alias-regressions.mjs
+```
+
+This standalone runner loads the real generated JS and WASM, outside Bun's
+coverage mocks. It exercises both extraction exports, map skipping with and
+without resolution, unresolved null/undefined, valid resolved dependencies,
+callback exceptions, getters, proxy reflection, malformed fields and hostile
+exception objects. Every fault case verifies the previous sheet is unchanged.
+Native tests cover every branch of the internal fault-precedence helper.
+
+## Explicit Compiler Source Type
+
+Both extraction functions accept an optional ninth argument after
+`import_aliases`: `sourceType?: 'compiled-mdx'`. Use it for project-compiled MDX
+under a custom extension, without changing the real filename:
+
+```ts
+const output = codeExtract(
+ 'src/page.mdown', compiledCode, '@devup-ui/react', 'df',
+ true, false, false, {}, 'compiled-mdx',
+)
+```
+
+The value means JavaScript with JSX, not TypeScript or raw Markdown. Omission
+keeps filename-derived parsing, including compiled JavaScript/JSX for `.md` and
+`.mdx`. Unknown extensions without an override fail with a located error even
+when the source has no styling import. Invalid source-type values also fail
+before stylesheet publication.
+
+Resolved modules may return `{ path, code, sourceType: 'compiled-mdx' }`. The
+child's own type reaches imported constants, reexports, stylesheet evaluation
+and stripping caches; it never inherits the importing file's override. This
+includes an ordinary TSX file importing a constant after JSX in compiled `.md`
+or `.mdown`. Parser/source-type failures preserve the previous stylesheet.
+Existing imported-value coordinate limitations remain owned by #757.
It is built from the Rust crates in `libs/` (`extractor`, `sheet`, `css`) with
`wasm-pack build --target nodejs`, so it loads with `require`/`import` in Node
and Bun, not in a browser.
@@ -36,12 +127,17 @@ Everything is exported from `pkg/index.js` (types in `pkg/index.d.ts`).
| `setDebug(debug)`, `isDebug()` | Readable class names instead of base-37 ones, with repeated declarations kept (the tests run this way). |
| `setModuleResolver(resolver)` | `(specifier, importer) => ({ path, code })`: how imports are resolved, so imported stylesheets are evaluated and imported constants inlined. |
| `setAtomHoist(threshold)`, `importFileRoutes(routes)` | Moves atoms used by `threshold` routes or more into the shared sheet. Call before `codeExtract`. |
-| `exportSheet`, `exportClassMap`, `exportFileMap`, `exportCanonicalMap` and the matching `import*` functions | Save the collected state and load it into another process, as a bundler does between its server and client builds. |
+| `exportSheet`, `exportClassMap`, `exportFileMap`, `exportCanonicalMap`, `exportReservationState` and the matching `import*` functions | Save an owner's collected state, including the reservation phase and miss count, across synchronous engine resets. |
+| `reserveClassSlots(rows, package, singleCss, importMainCssInCode, importMainCssInCss, importAliases)` | Reserve class/file counter slots without publishing CSS. Rows are `[filename, code, cssDir]` or `[filename, code, cssDir, 'compiled-mdx']`; compiled sources keep their real filenames. |
+| `getUnreservedClassSlotCount()` | Count new live allocator keys after reservation, excluding repeated keys. |
+| `getSuppressedReservationRowCount()` | Count failed rows suppressed by the latest reservation batch, independently of live misses. An empty or successful retry batch reports zero. |
+| `resetBuildState()` | Reset build-scoped engine state before a fresh build or owner restore. |
## State
The module keeps its state, the sheet, class maps, theme, prefix and debug flag,
-in one global per process. Two consequences:
+in one global per process. Integrations reset and restore their owned
+snapshots at synchronous operation boundaries. Two consequences:
- Register the theme, shorthands, prefix and resolver before the first
`codeExtract`, and extract the files of one build in one process.
@@ -52,13 +148,22 @@ in one global per process. Two consequences:
theme-token and Tailwind definition registries, and imported stylesheet caches. Call
only at a quiescent test boundary, never in a resolver callback or between
environments of a build that intentionally share state. Native caches reset
- on the calling thread.
+ on the calling thread. Opaque reservation snapshots must be restored with the
+ matching sheet and allocator maps, not imported into another owner's maps.
`resetBuildState()` remains the separate production build boundary. It clears
allocator bookkeeping, maps/routes, atom threshold/plan, prefix, sheet and resolver,
but leaves CSS theme-key configuration, shorthands and debug for the next build's
configuration path to overwrite. It is not an alias for the full testing reset.
+Scratch reservation failures are row-local: new class, variable, keyframe and
+other shared allocator keys, and extraction-time file inserts, are rolled back
+before the next row. The deterministic FILE inventory preseeded for the batch
+is retained, including failed rows. A later successful retry appends slots without
+renumbering existing keys. Suppression is captured in the opaque reservation
+snapshot and cleared by `resetBuildState`; it never changes the live-miss query.
+This rollback applies to scratch reservation only, not failed real transforms.
+
Rust tests use hidden `css::test_state::TestStateGuard` and
`extractor::test_state::TestStateGuard` support compiled into dependencies so
dependent-crate tests can restore owned snapshots on normal exit, nesting and
@@ -75,6 +180,24 @@ values must be literals, theme tokens or constants`` (the location is included
whenever the problem has one). A `.css.ts` stylesheet that throws while it runs
reports that exception.
-## License
+After rebuilding the bindings, run the real project-compiler/WASM matrix:
+
+```sh
+node bindings/devup-ui-wasm/source-type-regressions.mjs
+```
+
+The same JS runner also uses the installed `@mdx-js/mdx` compiler, resolved through
+`@mdx-js/loader` in `apps/landing` (no added dependency). Both default `_jsx`
+output and `jsx: true` output are extracted under real `.mdx` and `.md` filenames.
+It checks CSS, the relevance gate, source-map filename identity and rejection of
+raw Markdown. Compiled MDX is parsed as JSX JavaScript, not TSX. Its source-map
+coordinates refer to compiler output; bundler integrations must compose input
+maps to recover Markdown positions, or identify diagnostics as compiled MDX.
+
+Focused native MDX verification:
+
+```sh
+cargo test -p extractor mdx_ -- --nocapture
+```
Apache-2.0
diff --git a/bindings/devup-ui-wasm/boundary-regressions.mjs b/bindings/devup-ui-wasm/boundary-regressions.mjs
new file mode 100644
index 000000000..aabf08bc5
--- /dev/null
+++ b/bindings/devup-ui-wasm/boundary-regressions.mjs
@@ -0,0 +1,228 @@
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+import { assertScratchResolverFaults } from './scratch-reservation-regressions.mjs'
+
+const require = createRequire(import.meta.url)
+const wasm = require('./pkg/index.js')
+const filename = '/src/boundary.tsx'
+const source = `import { Box } from '@devup-ui/react';
+import { PRIMARY } from './tokens';
+export const view = ;`
+const extract = (method, code = source) =>
+ wasm[method](filename, code, '@devup-ui/react', 'df', true, false, false, {})
+
+const loaderPath = require.resolve('@mdx-js/loader', {
+ paths: [fileURLToPath(new URL('../../apps/landing/', import.meta.url))],
+})
+const compilerPath = createRequire(loaderPath).resolve('@mdx-js/mdx')
+const { compile } = await import(pathToFileURL(compilerPath).href)
+const markdown = `import { Box } from '@devup-ui/react'
+
+# Heading
+
+Hello
+`
+for (const jsx of [false, true]) {
+ const compiled = String(
+ await compile({ path: '/src/document.mdx', value: markdown }, { jsx }),
+ )
+ for (const extension of ['mdx', 'md']) {
+ for (const method of ['codeExtract', 'codeExtractWithoutSourceMap']) {
+ wasm.resetBuildState()
+ const path = `/src/document.${extension}`
+ assert.equal(wasm.hasDevupUI(path, compiled, '@devup-ui/react'), true)
+ const output = wasm[method](
+ path,
+ compiled,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ assert.match(output.code, /className/)
+ assert.doesNotMatch(output.code, /(?:_jsx\(\s*Box\b|
+ wasm.codeExtract(
+ path,
+ markdown,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ ),
+ (error) => {
+ assert.ok(error instanceof Error)
+ assert.ok(error.message.startsWith(`${path}:`))
+ assert.match(error.message, /:\d+:\d+:/)
+ return true
+ },
+ )
+}
+console.info(
+ 'Real MDX compiler regressions passed (default JSX calls and jsx:true).',
+)
+
+for (const jsx of [false, true]) {
+ const markdown =
+ "import { css } from '@emotion/react'\n\nexport const cls = css({ color: 'red' })\n\n\n"
+ const compiled = String(
+ await compile({ path: '/src/alias.mdx', value: markdown }, { jsx }),
+ )
+ wasm.resetBuildState()
+ const output = wasm.codeExtract(
+ '/src/alias.mdx',
+ compiled,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ { '@emotion/react': null },
+ )
+ assert.doesNotMatch(output.code, /@emotion\/react/)
+ assert.match(wasm.getCss(undefined, false), /color:red/)
+ output.free()
+}
+
+for (const method of ['codeExtract', 'codeExtractWithoutSourceMap']) {
+ wasm.resetBuildState()
+ const direct = extract(
+ method,
+ "import { Box } from '@devup-ui/react'; export const view = ;",
+ )
+ if (method === 'codeExtractWithoutSourceMap')
+ assert.equal(direct.map, undefined)
+ else assert.equal(typeof direct.map, 'string')
+ direct.free()
+ for (const result of [null, undefined]) {
+ wasm.resetBuildState()
+ wasm.setModuleResolver(() => result)
+ const output = extract(method)
+ assert.match(output.code, /--/)
+ if (method === 'codeExtractWithoutSourceMap')
+ assert.equal(output.map, undefined)
+ else assert.equal(typeof output.map, 'string')
+ output.free()
+ }
+
+ wasm.resetBuildState()
+ wasm.setModuleResolver(() => ({
+ path: '/src/tokens.ts',
+ code: "export const PRIMARY = 'blue';",
+ }))
+ const resolved = extract(method)
+ assert.deepEqual(resolved.dependencies, ['/src/tokens.ts'])
+ assert.doesNotMatch(resolved.code, /--/)
+ if (method === 'codeExtractWithoutSourceMap')
+ assert.equal(resolved.map, undefined)
+ resolved.free()
+
+ const faults = [
+ [
+ () => {
+ throw new Error('callback failure')
+ },
+ /callback failure/,
+ ],
+ [
+ () => {
+ throw 'string failure'
+ },
+ /string failure/,
+ ],
+ [
+ () => ({
+ get path() {
+ throw new Error('path getter failure')
+ },
+ }),
+ /path getter failure/,
+ ],
+ [
+ () => ({
+ path: '/src/tokens.ts',
+ get code() {
+ throw new Error('code getter failure')
+ },
+ }),
+ /code getter failure/,
+ ],
+ [
+ () =>
+ new Proxy(
+ {},
+ {
+ get() {
+ throw new Error('reflection failure')
+ },
+ },
+ ),
+ /reflection failure/,
+ ],
+ [() => 7, /Reflect|reflect|object/i],
+ [() => ({}), /path.*must be a string/],
+ [() => ({ path: 7, code: '' }), /path.*must be a string/],
+ [() => ({ path: '/src/tokens.ts', code: null }), /code.*must be a string/],
+ [() => ({ path: '/src/tokens.ts', code: 7 }), /code.*must be a string/],
+ [
+ () => {
+ throw new Proxy(
+ {},
+ {
+ get() {
+ throw new Error('hostile exception')
+ },
+ },
+ )
+ },
+ /unprintable JavaScript exception/,
+ ],
+ ]
+ for (const [resolver, cause] of faults) {
+ wasm.resetBuildState()
+ const initial = extract(
+ method,
+ "import { Box } from '@devup-ui/react'; export const view = ;",
+ )
+ initial.free()
+ const before = wasm.exportSheet()
+ wasm.setModuleResolver(resolver)
+ assert.throws(
+ () => extract(method),
+ (error) => {
+ assert.ok(error instanceof Error)
+ assert.match(
+ error.message,
+ /^\/src\/boundary\.tsx:1:1: module resolver cannot use `\.\/tokens` at build time: /,
+ )
+ assert.match(error.message, cause)
+ return true
+ },
+ )
+ assert.equal(wasm.exportSheet(), before)
+ }
+
+ assertScratchResolverFaults(wasm, method)
+}
+wasm.resetBuildState()
+console.info(
+ 'WASM boundary regressions passed (real generated JS/WASM, no mocks).',
+)
diff --git a/bindings/devup-ui-wasm/ignored-alias-regressions.mjs b/bindings/devup-ui-wasm/ignored-alias-regressions.mjs
new file mode 100644
index 000000000..76b50fa45
--- /dev/null
+++ b/bindings/devup-ui-wasm/ignored-alias-regressions.mjs
@@ -0,0 +1,235 @@
+import assert from 'node:assert/strict'
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+
+import { createModuleResolver } from '../../packages/plugin-utils/dist/index.mjs'
+import { verifyIgnoredSemantics } from './ignored-semantics-regressions.mjs'
+
+const wasm = createRequire(import.meta.url)('./pkg/index.js')
+const filename = '/src/ignored.tsx'
+const root = mkdtempSync(join(tmpdir(), 'devup-ignored-'))
+const file = (name, code) => {
+ const path = join(root, name)
+ writeFileSync(path, code)
+ return path
+}
+const barrel = file(
+ 'barrel.js',
+ "export {default as def, missing as value} from 'empty'; export * as ns from 'empty'; export * from 'empty';",
+)
+const cjs = file(
+ 'cjs.js',
+ "const ns = require('empty'); const {missing} = require('empty'); exports.value = missing; exports.other = ns.absent;",
+)
+const whole = file('whole.js', "module.exports = require('empty');")
+const real = file('real.js', 'module.exports = {};')
+const shared = createModuleResolver({
+ cwd: root,
+ alias: { empty: false, barrel, cjs, whole, real },
+})
+try {
+ for (const method of ['codeExtract', 'codeExtractWithoutSourceMap']) {
+ verifyIgnoredSemantics(wasm, method, shared)
+ wasm.resetBuildState()
+ wasm.setModuleResolver(() => ({ ignored: true }))
+ const output = wasm[method](
+ filename,
+ "import { Box } from '@devup-ui/react'; import { missing } from 'empty'; export const view = ",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ assert.deepEqual(output.dependencies, [])
+ assert.doesNotMatch(output.code, /--/)
+ assert.doesNotMatch(wasm.getCss(undefined, false), /color:/)
+ output.free()
+ for (const binding of [
+ "import * as ns from 'empty'; const value = ns.missing;",
+ "import def from 'empty'; const value = def.missing;",
+ "import {value} from 'barrel';",
+ "import {ns} from 'barrel'; const value = ns.missing;",
+ "import {value} from 'cjs';",
+ "import {other as value} from 'cjs';",
+ "import def from 'whole'; const value = def.missing;",
+ ]) {
+ wasm.resetBuildState()
+ wasm.setModuleResolver(shared)
+ const resolved = wasm[method](
+ filename,
+ `import {Box} from '@devup-ui/react'; ${binding} export const view = ;`,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ assert.doesNotMatch(resolved.code, /--/)
+ assert.doesNotMatch(wasm.getCss(undefined, false), /color:/)
+ if (method === 'codeExtractWithoutSourceMap')
+ assert.equal(resolved.map, undefined)
+ else assert.equal(typeof resolved.map, 'string')
+ resolved.free()
+ }
+ wasm.resetBuildState()
+ wasm.setModuleResolver(shared)
+ const evaluated = wasm[method](
+ '/src/ignored.css.ts',
+ "import {style} from '@devup-ui/react'; import def, {missing} from 'empty'; import * as ns from 'empty'; import {ns as indirect, def as indirectDefault, value} from 'barrel'; function color() { if (missing !== undefined || value !== undefined || Object.keys(ns).length || Object.keys(def).length || Object.keys(indirect).length || Object.keys(indirectDefault).length) throw new Error('bad interop'); return 'red'; } export const cls = style({color: color()});",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ assert.match(wasm.getCss(undefined, false), /color:red/)
+ assert.deepEqual(evaluated.dependencies, [barrel])
+ evaluated.free()
+
+ wasm.resetBuildState()
+ const ignored = {
+ ignored: true,
+ get path() {
+ throw new Error('must not read path')
+ },
+ get code() {
+ throw new Error('must not read code')
+ },
+ get sourceType() {
+ throw new Error('must not read sourceType')
+ },
+ }
+ wasm.setModuleResolver(() => ignored)
+ const bypassed = wasm[method](
+ filename,
+ "import {Box} from '@devup-ui/react'; import {missing} from 'empty'; export const view = ;",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ assert.deepEqual(bypassed.dependencies, [])
+ bypassed.free()
+
+ for (const result of [
+ true,
+ false,
+ 1,
+ 'ignored',
+ { ignored: false },
+ { ignored: null },
+ { ignored: 1 },
+ { ignored: 'true' },
+ {
+ get ignored() {
+ throw new Error('ignored getter first')
+ },
+ },
+ new Proxy(
+ {},
+ {
+ get() {
+ throw new Error('ignored proxy first')
+ },
+ },
+ ),
+ ]) {
+ wasm.resetBuildState()
+ wasm.setModuleResolver(undefined)
+ const initial = wasm[method](
+ filename,
+ "import {Box} from '@devup-ui/react'; export const view = ;",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ initial.free()
+ const sheet = wasm.exportSheet()
+ let calls = 0
+ wasm.setModuleResolver(() => {
+ calls += 1
+ return result
+ })
+ assert.throws(
+ () =>
+ wasm[method](
+ filename,
+ "import {Box} from '@devup-ui/react'; import {missing} from 'empty'; import {other} from 'later'; export const view = ;",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ ),
+ /module resolver cannot use `empty` at build time/,
+ )
+ assert.equal(calls, 1)
+ assert.equal(wasm.exportSheet(), sheet)
+ }
+
+ wasm.resetBuildState()
+ wasm.setModuleResolver(shared)
+ const previous = wasm[method](
+ filename,
+ "import {Box} from '@devup-ui/react'; export const view = ;",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ previous.free()
+ const sheet = wasm.exportSheet()
+ assert.throws(
+ () =>
+ wasm[method](
+ filename,
+ "import {css} from '@devup-ui/react'; import def from 'empty'; def.color = 'red'; export const cls = css(def);",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ ),
+ /changed/,
+ )
+ assert.equal(wasm.exportSheet(), sheet)
+ }
+ mkdirSync(join(root, 'src'))
+ const request = 'private-next-instrumentation-client-user'
+ const next = createModuleResolver({
+ cwd: root,
+ alias: {
+ [request]: [
+ join(root, 'src/instrumentation-client'),
+ join(root, 'instrumentation-client'),
+ 'private-next-empty-module',
+ ],
+ 'private-next-empty-module': false,
+ },
+ })
+ assert.deepEqual(next(request, join(root, 'entry.js')), { ignored: true })
+ const instrumentation = file(
+ 'instrumentation-client.js',
+ "export const value = 'blue';",
+ )
+ assert.equal(next(request, join(root, 'entry.js')).path, instrumentation)
+} finally {
+ wasm.setModuleResolver(undefined)
+ rmSync(root, { recursive: true, force: true })
+}
+console.info('Ignored alias actual WASM regressions passed.')
diff --git a/bindings/devup-ui-wasm/ignored-semantics-regressions.mjs b/bindings/devup-ui-wasm/ignored-semantics-regressions.mjs
new file mode 100644
index 000000000..62e89c7bb
--- /dev/null
+++ b/bindings/devup-ui-wasm/ignored-semantics-regressions.mjs
@@ -0,0 +1,97 @@
+import assert from 'node:assert/strict'
+
+export function verifyIgnoredSemantics(wasm, method, resolver) {
+ const extract = (filename, code) => {
+ wasm.resetBuildState()
+ wasm.setModuleResolver(resolver)
+ const output = wasm[method](
+ filename,
+ code,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ )
+ const result = {
+ code: output.code,
+ css: wasm.getCss(undefined, false),
+ dependencies: output.dependencies,
+ }
+ output.free()
+ return result
+ }
+ for (const key of [
+ 'constructor',
+ 'toString',
+ '__proto__',
+ 'hasOwnProperty',
+ ]) {
+ const named = extract(
+ '/src/named.css.ts',
+ `import {style} from '@devup-ui/react'; import {${key} as value} from 'empty'; export const cls = style({color: typeof value === 'undefined' ? 'red' : 'blue'});`,
+ )
+ assert.match(named.css, /color:red/, `${method}: named ${key}`)
+ assert.doesNotMatch(named.css, /color:blue/)
+ assert.deepEqual(named.dependencies, [])
+ }
+ for (const binding of [
+ "import {missing as value} from 'empty';",
+ "import def from 'empty'; const value = def.missing;",
+ "import * as ns from 'empty'; const value = ns.missing;",
+ "import {value} from 'barrel';",
+ "import def from 'whole'; const value = def.missing;",
+ 'const record = {}; const value = record.missing;',
+ ]) {
+ const shadowed = extract(
+ '/src/shadowed.tsx',
+ `import {Box} from '@devup-ui/react'; ${binding} export const View = (undefined, read) => ;`,
+ )
+ const argument = /read\(([^)]+)\)/.exec(shadowed.code)?.[1]
+ assert(argument, shadowed.code)
+ const selected = new Function(
+ 'undefined',
+ 'read',
+ 'value',
+ `return read(${argument});`,
+ )('blue', (value) => value ?? 'red', undefined)
+ assert.equal(selected, 'red', `${method}: ${binding}`)
+ }
+ for (const binding of [
+ "import def from 'real';",
+ 'const def = {};',
+ "import def from 'empty';",
+ "import * as def from 'empty';",
+ "import def from 'whole';",
+ ]) {
+ for (const [key, expected] of [
+ ['constructor', 'Object'],
+ ['toString', 'Object.prototype.toString'],
+ ]) {
+ const real = extract(
+ '/src/real.tsx',
+ `import {Box} from '@devup-ui/react'; ${binding} export const view = ;`,
+ )
+ const condition = /className=\{([^?]+)\?/.exec(real.code)?.[1]
+ assert(condition, real.code)
+ assert.equal(
+ new Function('def', `return ${condition};`)({}),
+ true,
+ `${method}: ${binding} ${key}`,
+ )
+ }
+ }
+ for (const binding of [
+ "import def from 'empty';",
+ "import * as def from 'empty';",
+ ]) {
+ const mutated = extract(
+ '/src/mutated.tsx',
+ `import {Box} from '@devup-ui/react'; ${binding} def.color = 'blue'; export const view = ;`,
+ )
+ assert.match(mutated.code, /def\.color/)
+ assert.match(mutated.css, /color:var\(/)
+ assert.deepEqual(mutated.dependencies, [])
+ }
+}
diff --git a/bindings/devup-ui-wasm/jsx-dev-regressions.mjs b/bindings/devup-ui-wasm/jsx-dev-regressions.mjs
new file mode 100644
index 000000000..e77913a3b
--- /dev/null
+++ b/bindings/devup-ui-wasm/jsx-dev-regressions.mjs
@@ -0,0 +1,118 @@
+import assert from 'node:assert/strict'
+import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { pathToFileURL } from 'node:url'
+
+const project = createRequire(
+ new URL('../../apps/landing/package.json', import.meta.url),
+)
+const compiler = createRequire(project.resolve('@mdx-js/loader'))
+const { compile } = await import(
+ pathToFileURL(compiler.resolve('@mdx-js/mdx')).href
+)
+const { parse } = compiler('acorn')
+const wasm = createRequire(import.meta.url)('./pkg/index.js')
+const markdown =
+ 'import { Box } from "@devup-ui/react"\n\n\n'
+let passed = 0
+let failed = 0
+
+function structure(value) {
+ return JSON.parse(
+ JSON.stringify(value, (key, item) =>
+ ['start', 'end', 'loc', 'raw'].includes(key) ? undefined : item,
+ ),
+ )
+}
+
+function calls(code) {
+ const found = []
+ function visit(node) {
+ if (!node || typeof node !== 'object') return
+ if (node.type === 'CallExpression' && node.callee.name === '_jsxDEV')
+ found.push(node)
+ for (const value of Object.values(node)) {
+ if (Array.isArray(value)) value.forEach(visit)
+ else visit(value)
+ }
+ }
+ visit(parse(code, { ecmaVersion: 'latest', sourceType: 'module' }))
+ return found
+}
+
+const root = mkdtempSync(join(tmpdir(), 'devup-jsxdev-'))
+try {
+ for (const extension of ['mdx', 'md', 'mdown']) {
+ const filename = join(root, `development.${extension}`).replaceAll(
+ '\\',
+ '/',
+ )
+ writeFileSync(filename, markdown)
+ const code = String(
+ await compile(
+ { path: filename, value: readFileSync(filename, 'utf8') },
+ { development: true, jsx: false, format: 'mdx' },
+ ),
+ )
+ const before = calls(code)
+ const componentCalls = before.flatMap((call, index) =>
+ call.arguments[0].name === 'Box' ? [index] : [],
+ )
+ assert.equal(componentCalls.length, 2)
+ assert.match(code, /react\/jsx-dev-runtime/)
+ for (const method of ['codeExtract', 'codeExtractWithoutSourceMap']) {
+ wasm.resetBuildState()
+ try {
+ const output = wasm[method](
+ filename,
+ code,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ extension === 'mdown' ? 'compiled-mdx' : undefined,
+ )
+ try {
+ const after = calls(output.code)
+ assert.equal(after.length, before.length)
+ for (const [index, original] of before.entries()) {
+ assert.equal(after[index].arguments.length, 6)
+ assert.deepEqual(
+ structure(after[index].callee),
+ structure(original.callee),
+ )
+ assert.deepEqual(
+ structure(after[index].arguments.slice(2)),
+ structure(original.arguments.slice(2)),
+ )
+ }
+ assert.match(wasm.getCss(undefined, false), /background:tomato/)
+ assert.match(wasm.getCss(undefined, false), /padding:8px/)
+ assert.doesNotMatch(output.code, /(?:;
+import {base} from './supplier.css'; import {FAULT} from './fault';
+export const tail=;`
+const good = `import {style} from '${PACKAGE}';
+import {base,token} from './supplier.css';
+export const own=style([base,{background:token}]);`
+
+function snapshot(wasm) {
+ return {
+ slots: JSON.parse(wasm.exportClassMap()),
+ files: JSON.parse(wasm.exportFileMap()),
+ sheet: JSON.parse(wasm.exportSheet()),
+ reservation: JSON.parse(wasm.exportReservationState()),
+ misses: wasm.getUnreservedClassSlotCount(),
+ }
+}
+
+export function assertScratchAllocationRollback(wasm, method) {
+ for (const singleCss of [false, true]) {
+ for (const kind of ['throw', 'malformed']) {
+ // Given: the control keeps the same FILE inventory, skipping only bad extraction.
+ const rows = [
+ ['/src/bad.tsx', bad, 'df'],
+ ['/src/good.css.ts', good, 'df'],
+ ]
+ const reserve = (inputs) =>
+ wasm.reserveClassSlots(inputs, PACKAGE, singleCss, false, false, {})
+ const extract = (row) =>
+ wasm[method](row[0], row[1], PACKAGE, 'df', singleCss, false, false, {})
+ let prepared = false
+ let partial = 0
+ const resolver = (specifier) => {
+ if (specifier === './fault') {
+ partial++
+ if (!prepared) {
+ if (kind === 'throw') throw new Error('late host fault')
+ return { path: '/src/fault.ts', code: null }
+ }
+ return { path: '/src/fault.ts', code: "export const FAULT='orchid';" }
+ }
+ assert.equal(specifier, './supplier.css')
+ return { path: '/src/supplier.css.ts', code: supplier }
+ }
+ const reset = () => {
+ wasm.resetBuildState()
+ wasm.setDebug(false)
+ wasm.setModuleResolver(resolver)
+ wasm.seedFileMap([
+ '/src/bad.tsx',
+ '/src/good.css.ts',
+ '/src/supplier.css.ts',
+ '/src/fault.ts',
+ ])
+ }
+ reset()
+ assert.throws(
+ () => extract(rows[0]),
+ /module resolver cannot use `\.\/fault`/,
+ )
+ assert.ok(
+ Object.values(snapshot(wasm).slots).reduce(
+ (sum, entries) => sum + Object.keys(entries).length,
+ 0,
+ ) >= 4,
+ 'the real faulting extraction allocates before its result is rejected',
+ )
+ reset()
+ partial = 0
+ reserve([[rows[0][0], '', 'df'], rows[1]])
+ const control = snapshot(wasm)
+ assert.equal(wasm.getSuppressedReservationRowCount(), 0)
+ reset()
+ // When
+ reserve(rows)
+ // Then
+ assert.equal(partial, 1)
+ const failed = snapshot(wasm)
+ assert.deepEqual(failed.slots, control.slots)
+ assert.deepEqual(failed.files, control.files)
+ assert.deepEqual(failed.sheet, control.sheet)
+ assert.deepEqual(failed.reservation, {
+ ...control.reservation,
+ suppressed: 1,
+ })
+ assert.equal(failed.misses, control.misses)
+ assert.equal(wasm.getSuppressedReservationRowCount(), 1)
+ assert.equal(wasm.getUnreservedClassSlotCount(), 0)
+
+ // Given / When / Then: an owner reset/restore carries suppression separately.
+ reset()
+ assert.equal(wasm.getSuppressedReservationRowCount(), 0)
+ wasm.importSheet(failed.sheet)
+ wasm.importClassMap(failed.slots)
+ wasm.importFileMap(failed.files)
+ wasm.importReservationState(failed.reservation)
+ assert.equal(wasm.getSuppressedReservationRowCount(), 1)
+ assert.equal(wasm.getUnreservedClassSlotCount(), 0)
+
+ // Given / When / Then: preparation on the next batch appends, never renumbers.
+ prepared = true
+ reserve(rows)
+ assert.equal(wasm.getSuppressedReservationRowCount(), 0)
+ const retried = snapshot(wasm)
+ assert.deepEqual(retried.files, failed.files)
+ assert.deepEqual(retried.sheet, failed.sheet)
+ for (const [file, entries] of Object.entries(failed.slots)) {
+ for (const [key, slot] of Object.entries(entries)) {
+ assert.equal(retried.slots[file][key], slot)
+ }
+ }
+ const count = (slots) =>
+ Object.values(slots).reduce(
+ (sum, entries) => sum + Object.keys(entries).length,
+ 0,
+ )
+ assert.ok(count(retried.slots) > count(failed.slots))
+ const output = extract(rows[0])
+ assert.doesNotMatch(output.code, /;`
+ for (const scratchFault of ['throw', 'malformed']) {
+ // Given
+ wasm.resetBuildState()
+ wasm[method](
+ filename,
+ "import {Box} from '@devup-ui/react'; ;",
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ ).free()
+ const before = wasm.exportSheet()
+ const calls = []
+ let prepared = false
+ const scratchSource =
+ "import {Box} from '@devup-ui/react'; import {PRIMARY} from './scratchA'; ;"
+ const suffixSource =
+ "import {Box} from '@devup-ui/react'; import {PRIMARY} from './ready'; ;"
+ const invoke = (path, code) =>
+ wasm[method](
+ path,
+ code,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ {},
+ 'compiled-mdx',
+ )
+ wasm.setModuleResolver((specifier, importer) => {
+ calls.push([specifier, importer])
+ if (specifier === './faultB') throw new Error('realB')
+ if (specifier === './scratchA' && !prepared) {
+ if (scratchFault === 'throw') throw new Error('scratchA')
+ return { path: '/src/prepared.mdown', code: null }
+ }
+ return {
+ path: '/src/prepared.mdown',
+ code: "const node=; export const PRIMARY='blue';",
+ sourceType: 'compiled-mdx',
+ }
+ })
+ // When
+ wasm.reserveClassSlots(
+ [
+ ['/src/scratch.mdown', scratchSource, 'df', 'compiled-mdx'],
+ ['/src/suffix.mdown', suffixSource, 'df', 'compiled-mdx'],
+ ],
+ '@devup-ui/react',
+ true,
+ false,
+ false,
+ {},
+ )
+ // Then
+ assert.deepEqual(calls, [
+ ['./scratchA', '/src/scratch.mdown'],
+ ['./ready', '/src/suffix.mdown'],
+ ['./ready', '/src/suffix.mdown'],
+ ])
+ assert.equal(wasm.exportSheet(), before)
+ assert.throws(
+ () => invoke(filename, source.replace('./tokens', './faultB')),
+ (error) => {
+ assert.ok(error instanceof Error)
+ assert.equal(
+ error.message,
+ '/src/boundary.tsx:1:1: module resolver cannot use `./faultB` at build time: realB',
+ )
+ return true
+ },
+ )
+ assert.equal(wasm.exportSheet(), before)
+ prepared = true
+ wasm.reserveClassSlots(
+ [
+ ['/src/scratch.mdown', scratchSource, 'df', 'compiled-mdx'],
+ ['/src/suffix.mdown', suffixSource, 'df', 'compiled-mdx'],
+ ],
+ '@devup-ui/react',
+ true,
+ false,
+ false,
+ {},
+ )
+ assert.equal(wasm.exportSheet(), before)
+ const output = invoke('/src/scratch.mdown', scratchSource)
+ assert.deepEqual(output.dependencies, ['/src/prepared.mdown'])
+ assert.doesNotMatch(output.code, /--/)
+ assert.match(wasm.getCss(undefined, false), /color:blue/)
+ output.free()
+ }
+}
diff --git a/bindings/devup-ui-wasm/source-type-regressions.mjs b/bindings/devup-ui-wasm/source-type-regressions.mjs
new file mode 100644
index 000000000..d8e1a36ff
--- /dev/null
+++ b/bindings/devup-ui-wasm/source-type-regressions.mjs
@@ -0,0 +1,248 @@
+import './jsx-dev-regressions.mjs'
+
+import assert from 'node:assert/strict'
+import { createRequire } from 'node:module'
+import { pathToFileURL } from 'node:url'
+
+const require = createRequire(
+ new URL('../../apps/landing/package.json', import.meta.url),
+)
+const compiler = createRequire(require.resolve('@mdx-js/loader')).resolve(
+ '@mdx-js/mdx',
+)
+const { compile } = await import(pathToFileURL(compiler).href)
+const wasm = createRequire(import.meta.url)('./pkg/index.js')
+const methods = ['codeExtract', 'codeExtractWithoutSourceMap']
+let passed = 0
+
+function check(name, action) {
+ wasm.resetBuildState()
+ action()
+ passed += 1
+ console.info(`PASS ${name}`)
+}
+
+function extract(method, filename, code, sourceType, aliases = {}) {
+ return wasm[method](
+ filename,
+ code,
+ '@devup-ui/react',
+ 'df',
+ true,
+ false,
+ false,
+ aliases,
+ sourceType,
+ )
+}
+
+const markdown =
+ 'import { Box } from "@devup-ui/react"\n\n\n'
+const aliasMarkdown =
+ 'import { css } from "@emotion/react"\n\nexport const cls = css({color: "blue"})\n\n\n'
+for (const jsx of [false, true]) {
+ const code = String(
+ await compile(
+ { path: '/src/page.mdown', value: markdown },
+ { jsx, format: 'mdx' },
+ ),
+ )
+ const alias = String(
+ await compile(
+ { path: '/src/alias.mdown', value: aliasMarkdown },
+ { jsx, format: 'mdx' },
+ ),
+ )
+ for (const method of methods) {
+ for (const extension of ['md', 'mdx', 'mdown', 'ts']) {
+ const filename = `/src/page.${extension}`
+ check(`${method}:${jsx}:${extension}`, () => {
+ const output = extract(
+ method,
+ filename,
+ code,
+ ['mdown', 'ts'].includes(extension) ? 'compiled-mdx' : undefined,
+ )
+ try {
+ assert.match(wasm.getCss(undefined, false), /background:tomato/)
+ assert.doesNotMatch(output.code, /(?: {
+ const output = extract(
+ method,
+ '/src/alias.mdown',
+ alias,
+ 'compiled-mdx',
+ { '@emotion/react': null },
+ )
+ try {
+ assert.match(wasm.getCss(undefined, false), /color:blue/)
+ } finally {
+ output.free()
+ }
+ })
+ for (const reexport of [false, true]) {
+ check(`${method}:${jsx}:imported:${reexport}`, () => {
+ wasm.setModuleResolver((specifier) =>
+ specifier === './entry' && reexport
+ ? {
+ path: '/src/reexport.js',
+ code: "export {PRIMARY} from './tokens';",
+ }
+ : {
+ path: '/src/tokens.mdown',
+ code: `${code}\nexport const PRIMARY = 'blue';`,
+ sourceType: 'compiled-mdx',
+ },
+ )
+ const output = extract(
+ method,
+ '/src/App.tsx',
+ 'import {Box} from "@devup-ui/react"; import {PRIMARY} from "./entry"; export const view = ;',
+ )
+ try {
+ assert.match(wasm.getCss(undefined, false), /color:blue/)
+ assert.doesNotMatch(wasm.getCss(undefined, false), /var\(--/)
+ assert.ok(output.dependencies.includes('/src/tokens.mdown'))
+ } finally {
+ output.free()
+ }
+ })
+ }
+ }
+}
+for (const method of methods) {
+ const stylesheetModule = String(
+ await compile(
+ { path: '/src/value.mdown', value: '# Heading' },
+ { jsx: true, format: 'mdx' },
+ ),
+ )
+ check(`${method}:stylesheet-import`, () => {
+ wasm.setModuleResolver(() => ({
+ path: '/src/value.mdown',
+ code: `${stylesheetModule}\nexport const PRIMARY = 'blue';`,
+ sourceType: 'compiled-mdx',
+ }))
+ const output = extract(
+ method,
+ '/src/theme.css.ts',
+ 'import {style} from "@vanilla-extract/css"; import {PRIMARY} from "./value"; export const cls = style({color: PRIMARY});',
+ undefined,
+ { '@vanilla-extract/css': null },
+ )
+ try {
+ assert.match(wasm.getCss(undefined, false), /color:blue/)
+ } finally {
+ output.free()
+ }
+ })
+ for (const value of ['invalid', '', 7, {}, true]) {
+ check(`${method}:invalid-root:${JSON.stringify(value)}`, () => {
+ const previous = extract(
+ method,
+ '/src/plain.js',
+ 'import {css} from "@devup-ui/react"; export const cls = css({color: "green"});',
+ )
+ previous.free()
+ const sheet = wasm.exportSheet()
+ assert.throws(
+ () =>
+ extract(method, '/src/plain.js', 'export const answer = 42;', value),
+ /\/src\/plain\.js:1:1:.*source.*type/i,
+ )
+ assert.equal(wasm.exportSheet(), sheet)
+ })
+ }
+ check(`${method}:unknown-fastpath`, () => {
+ assert.throws(
+ () => extract(method, '/src/plain.mdown', 'export const answer = 42;'),
+ /\/src\/plain\.mdown:1:1:.*Unknown file extension/,
+ )
+ })
+ for (const value of ['invalid', 7]) {
+ check(`${method}:invalid-child:${value}`, () => {
+ const initial = extract(
+ method,
+ '/src/App.tsx',
+ 'import {Box} from "@devup-ui/react"; export const view = ;',
+ )
+ initial.free()
+ const sheet = wasm.exportSheet()
+ wasm.setModuleResolver(() => ({
+ path: '/src/token.js',
+ code: "export const PRIMARY = 'blue';",
+ sourceType: value,
+ }))
+ assert.throws(
+ () =>
+ extract(
+ method,
+ '/src/App.tsx',
+ 'import {Box} from "@devup-ui/react"; import {PRIMARY} from "./token"; export const view = ;',
+ ),
+ /\/src\/App\.tsx:1:1:.*source.*type/i,
+ )
+ assert.equal(wasm.exportSheet(), sheet)
+ })
+ }
+ check(`${method}:getter`, () => {
+ wasm.setModuleResolver(() => ({
+ path: '/src/token.js',
+ code: "export const PRIMARY = 'blue';",
+ get sourceType() {
+ throw new Error('sourceType getter fault')
+ },
+ }))
+ assert.throws(
+ () =>
+ extract(
+ method,
+ '/src/App.tsx',
+ 'import {Box} from "@devup-ui/react"; import {PRIMARY} from "./token"; export const view = ;',
+ ),
+ /\/src\/App\.tsx:1:1:.*sourceType getter fault/,
+ )
+ })
+ check(`${method}:root-mode-not-inherited`, () => {
+ wasm.setModuleResolver(() => ({
+ path: '/src/token.ts',
+ code: "export enum Colors { PRIMARY = 'blue' }",
+ }))
+ const output = extract(
+ method,
+ '/src/App.mdown',
+ 'import {Box} from "@devup-ui/react"; import {Colors} from "./token"; export const view = ;',
+ 'compiled-mdx',
+ )
+ try {
+ assert.match(wasm.getCss(undefined, false), /color:blue/)
+ } finally {
+ output.free()
+ }
+ })
+ check(`${method}:unknown-child`, () => {
+ wasm.setModuleResolver(() => ({
+ path: '/src/token.custom',
+ code: "export const PRIMARY = 'blue';",
+ }))
+ assert.throws(
+ () =>
+ extract(
+ method,
+ '/src/App.tsx',
+ 'import {Box} from "@devup-ui/react"; import {PRIMARY} from "./token"; export const view = ;',
+ ),
+ /\/src\/token\.custom:1:1:.*Unknown file extension/,
+ )
+ })
+}
+wasm.resetBuildState()
+console.info(JSON.stringify({ passed }))
diff --git a/bindings/devup-ui-wasm/src/atom_hoist_measurement.rs b/bindings/devup-ui-wasm/src/atom_hoist_measurement.rs
index 972a90b51..1ac7ec65e 100644
--- a/bindings/devup-ui-wasm/src/atom_hoist_measurement.rs
+++ b/bindings/devup-ui-wasm/src/atom_hoist_measurement.rs
@@ -137,6 +137,7 @@ fn build(modules: &[Module], hoist: bool) -> Result {
filename: module.path.clone(),
code: module.source(),
css_dir: "df".into(),
+ source_type: None,
})
.collect();
inventory.sort_by(|a, b| a.filename.cmp(&b.filename));
@@ -153,6 +154,7 @@ fn build(modules: &[Module], hoist: bool) -> Result {
false,
true,
HashMap::new(),
+ None,
)
.map(|output| output.code())
.map_err(|message| ExtractionError {
diff --git a/bindings/devup-ui-wasm/src/atom_tests.rs b/bindings/devup-ui-wasm/src/atom_tests.rs
index 1fa448abb..0ede30266 100644
--- a/bindings/devup-ui-wasm/src/atom_tests.rs
+++ b/bindings/devup-ui-wasm/src/atom_tests.rs
@@ -35,6 +35,7 @@ fn compile(file: &str, source: &str) -> Output {
"@vanilla-extract/css".to_string(),
ImportAlias::NamedToNamed,
)]),
+ None,
)
.unwrap_or_else(|error| panic!("{error}"))
}
@@ -56,6 +57,7 @@ fn reserve(sources: &[(&str, &str)]) {
filename: (*file).into(),
code: (*source).into(),
css_dir: "df".into(),
+ source_type: None,
})
.collect();
reserve_class_slots_internal(
diff --git a/bindings/devup-ui-wasm/src/atom_tests/lifecycle.rs b/bindings/devup-ui-wasm/src/atom_tests/lifecycle.rs
index 42a577b10..488c7e708 100644
--- a/bindings/devup-ui-wasm/src/atom_tests/lifecycle.rs
+++ b/bindings/devup-ui-wasm/src/atom_tests/lifecycle.rs
@@ -102,9 +102,12 @@ fn canonical_bucket_reach_is_frozen_before_imported_styles_are_processed() {
("peer.tsx".into(), HashSet::from([1])),
]));
let resolver = |specifier: &str, _importer: &str| {
- (specifier == "./tokens").then(|| ResolvedModule {
- path: "tokens.ts".to_string(),
- code: "export const styles={color:'red',_hover:{color:'blue'}};".to_string(),
+ (specifier == "./tokens").then(|| {
+ extractor::ModuleResolution::Resolved(ResolvedModule {
+ path: "tokens.ts".to_string(),
+ code: "export const styles={color:'red',_hover:{color:'blue'}};".to_string(),
+ source_type: None,
+ })
})
};
// When
@@ -117,6 +120,7 @@ fn canonical_bucket_reach_is_frozen_before_imported_styles_are_processed() {
false,
false,
HashMap::new(),
+ None,
&resolver,
)
.unwrap_or_else(|error| panic!("{error}"));
@@ -186,6 +190,7 @@ fn keyframe_content_and_scope_resolve_when_using_single_css() {
filename: "a.tsx".into(),
code: source.into(),
css_dir: "df".into(),
+ source_type: None,
}],
ExtractOption {
single_css: true,
@@ -203,6 +208,7 @@ fn keyframe_content_and_scope_resolve_when_using_single_css() {
false,
false,
HashMap::new(),
+ None,
)
.unwrap_or_else(|error| panic!("{error}"));
// Then
diff --git a/bindings/devup-ui-wasm/src/atom_tests/single_css.rs b/bindings/devup-ui-wasm/src/atom_tests/single_css.rs
index 84bd048df..a71a8a2c1 100644
--- a/bindings/devup-ui-wasm/src/atom_tests/single_css.rs
+++ b/bindings/devup-ui-wasm/src/atom_tests/single_css.rs
@@ -16,6 +16,7 @@ fn build(atom: bool, debug: bool) -> (String, String, String) {
filename: "a.tsx".into(),
code: SOURCE_A.into(),
css_dir: "df".into(),
+ source_type: None,
}],
option,
None,
@@ -29,6 +30,7 @@ fn build(atom: bool, debug: bool) -> (String, String, String) {
false,
false,
HashMap::new(),
+ None,
)
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(get_unreserved_class_slot_count(), 0);
@@ -63,6 +65,7 @@ fn w66_single_css_does_not_freeze_empty_reach_before_per_file_reservation() {
filename: "a.tsx".into(),
code: RED.into(),
css_dir: "df".into(),
+ source_type: None,
}];
// When
reserve_class_slots_internal(
@@ -110,12 +113,14 @@ fn single_css_error_and_resolver_unwind_restore_effective_atom_mode() {
assert!(error.to_string().contains("bad.tsx:"));
assert!(css::atom_hoist::is_atom_hoist());
assert_eq!(css::atom_hoist::atom_plan(), None);
- let resolver = |_: &str, _: &str| -> Option { panic!("resolver unwind") };
+ let resolver =
+ |_: &str, _: &str| -> Option { panic!("resolver unwind") };
let unwind = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
reserve_class_slots_internal(&[ReservationInput {
filename: "root.css.ts".into(),
code: "import {style} from '@devup-ui/react'; import {base} from './supplier.css'; export const own=style(base);".into(),
css_dir: "df".into(),
+ source_type: None,
}], option, Some(&resolver));
}));
assert!(unwind.is_err());
diff --git a/bindings/devup-ui-wasm/src/cache_protocol_tests.rs b/bindings/devup-ui-wasm/src/cache_protocol_tests.rs
index b3545e669..3eccbd7ea 100644
--- a/bindings/devup-ui-wasm/src/cache_protocol_tests.rs
+++ b/bindings/devup-ui-wasm/src/cache_protocol_tests.rs
@@ -66,6 +66,7 @@ fn exported_generation_roundtrips_frozen_atom_placement() {
false,
false,
HashMap::new(),
+ None,
)
.unwrap_or_else(|error| panic!("{error}"));
}
diff --git a/bindings/devup-ui-wasm/src/css_output_edge_tests.rs b/bindings/devup-ui-wasm/src/css_output_edge_tests.rs
index 625a21918..ad0e6d607 100644
--- a/bindings/devup-ui-wasm/src/css_output_edge_tests.rs
+++ b/bindings/devup-ui-wasm/src/css_output_edge_tests.rs
@@ -29,6 +29,7 @@ fn compile_file(filename: &str, source: &str) -> Result