From 1d67ab67595f69dec05c529dc1b95750d010ac9d Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 9 Oct 2026 17:41:35 +0900 Subject: [PATCH 1/2] fix(wasm): skip source maps in the no-map export Refs #686 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- bindings/devup-ui-wasm/src/lib.rs | 2 +- .../src/__tests__/source-map.wasm.test.ts | 263 ++++++++++++++++++ 2 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 packages/next-plugin/src/__tests__/source-map.wasm.test.ts diff --git a/bindings/devup-ui-wasm/src/lib.rs b/bindings/devup-ui-wasm/src/lib.rs index 0a2347ac2..0ffb62ce1 100644 --- a/bindings/devup-ui-wasm/src/lib.rs +++ b/bindings/devup-ui-wasm/src/lib.rs @@ -591,7 +591,7 @@ pub fn code_extract_without_source_map( import_main_css_in_code, import_main_css_in_css, import_aliases, - SourceMapMode::Generate, + SourceMapMode::Skip, ) } diff --git a/packages/next-plugin/src/__tests__/source-map.wasm.test.ts b/packages/next-plugin/src/__tests__/source-map.wasm.test.ts new file mode 100644 index 000000000..dd404834e --- /dev/null +++ b/packages/next-plugin/src/__tests__/source-map.wasm.test.ts @@ -0,0 +1,263 @@ +import { existsSync, mkdtempSync, rmSync, watch } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, relative } from 'node:path' + +import * as wasm from '@devup-ui/wasm' +import { describe, expect, it } from 'bun:test' + +import { + flushCoordinatorWrites, + type PrewarmedOutput, + resetCoordinator, + startCoordinator, + takeExtractOutput, +} from '../coordinator' +import loader, { resetInit } from '../loader' +import { withModuleResolver } from '../wasm' + +const source = `import { Box } from '@devup-ui/react'; +export const box = ;` + +function captureState() { + const sheet = wasm.exportSheet() + const classes = wasm.exportClassMap() + const files = wasm.exportFileMap() + const canonical = wasm.exportCanonicalMap() + const prefix = wasm.getPrefix() + const debug = wasm.isDebug() + return () => { + wasm.importSheet(JSON.parse(sheet)) + wasm.importClassMap(JSON.parse(classes)) + wasm.importFileMap(JSON.parse(files)) + wasm.importCanonicalMap(JSON.parse(canonical)) + wasm.setPrefix(prefix) + wasm.setDebug(debug) + } +} + +async function withState(run: (root: string) => void | Promise) { + const restore = captureState() + const root = mkdtempSync(join(tmpdir(), 'devup-ui-w48-map-')) + try { + wasm.importCanonicalMap({}) + wasm.setPrefix('w48-') + wasm.setDebug(false) + wasm.setModuleResolver(undefined) + await run(root) + } finally { + try { + await flushCoordinatorWrites() + } finally { + resetCoordinator() + resetInit() + restore() + // There is no resolver getter; restore Next's standard resolver policy. + withModuleResolver(wasm) + rmSync(root, { recursive: true, force: true }) + } + } +} + +describe('real WASM source-map modes', () => { + it.each([false, true])( + 'omits maps without changing extraction bytes when resolver is %s', + async (resolveImports) => { + await withState((root) => { + // Given identical initial WASM state for both extraction modes. + const filename = join(root, 'box.tsx').replaceAll('\\', '/') + const tokenFile = join(root, 'tokens.ts').replaceAll('\\', '/') + const input = resolveImports + ? `import { color } from './tokens'; ${source.replace('color="red"', 'color={color}')}` + : source + const resolved: string[] = [] + if (resolveImports) { + wasm.setModuleResolver((specifier: string, importer: string) => { + if (specifier !== './tokens' || importer !== filename) + return undefined + resolved.push(importer) + return { path: tokenFile, code: 'export const color = "red";' } + }) + } + const restoreBaseline = captureState() + const args = [ + filename, + input, + '@devup-ui/react', + './df', + false, + false, + true, + {}, + ] as const + const generated = takeExtractOutput(wasm.codeExtract(...args)) + const generatedCss = wasm.getCss(null, false) + restoreBaseline() + + // When the actual JS/WASM no-map export handles the same input. + const skipped = takeExtractOutput( + wasm.codeExtractWithoutSourceMap(...args), + ) + + // Then only the map differs, not JSX, CSS, metadata or dependencies. + expect(skipped.map).toBeUndefined() + expect(generated.map).toEqual(expect.any(String)) + expect(JSON.parse(generated.map ?? 'null')).toMatchObject({ + version: 3, + }) + const { map: generatedMap, ...generatedBytes } = generated + const { map: skippedMap, ...skippedBytes } = skipped + expect(skippedBytes).toEqual(generatedBytes) + expect(wasm.getCss(null, false)).toBe(generatedCss) + expect(skipped.code).not.toContain(' { + await withState((root) => { + // Given a fresh filename with transformable JSX. + const filename = join(root, 'positive.tsx') + // When the actual map-enabled export runs. + const output = takeExtractOutput( + wasm.codeExtract( + filename, + source, + '@devup-ui/react', + './df', + true, + false, + true, + {}, + ), + ) + // Then a real map retains the original source, rather than an empty token. + expect(JSON.parse(output.map ?? 'null')).toMatchObject({ + version: 3, + sourcesContent: [source], + mappings: expect.any(String), + }) + }) + }) +}) + +describe.each([false, true])('Next singleCss with prewarm=%s', (prewarm) => { + it.each([false, true])( + 'forwards the correct loader callback map when sourceMap=%s', + async (sourceMap) => { + await withState(async (root) => { + // Given real extracted output, optionally stored in the prewarm cache. + const resourcePath = join(root, 'page.tsx') + const filename = relative(process.cwd(), resourcePath).replaceAll( + '\\', + '/', + ) + const coordinatorPortFile = join(root, 'port') + const restoreBaseline = captureState() + const args = [ + filename, + source, + '@devup-ui/react', + './', + true, + false, + true, + {}, + ] as const + const generated = takeExtractOutput(wasm.codeExtract(...args)) + restoreBaseline() + const prewarmedOutputs = new Map() + if (prewarm) { + const extract = sourceMap + ? wasm.codeExtract + : wasm.codeExtractWithoutSourceMap + const output = takeExtractOutput(extract(...args)) + prewarmedOutputs.set(filename, { ...output, source }) + } + const watcher = watch(root) + let readinessTimeout: ReturnType | undefined + const ready = new Promise((resolve, reject) => { + watcher.on('error', reject) + readinessTimeout = setTimeout( + () => reject(new Error('Coordinator port file was not ready')), + 3000, + ) + watcher.on('change', () => { + if (existsSync(coordinatorPortFile)) resolve() + }) + }) + const options = { + package: '@devup-ui/react', + cssDir: root, + sheetFile: join(root, 'sheet.json'), + classMapFile: join(root, 'classes.json'), + fileMapFile: join(root, 'files.json'), + singleCss: true, + coordinatorPortFile, + } + let coordinator: ReturnType | undefined + try { + coordinator = startCoordinator({ + ...options, + wasm, + sourceMap, + importAliases: {}, + canonicalMap: {}, + prewarmedOutputs, + prewarmedFiles: prewarm ? [filename] : [], + expectedBaseFiles: [filename], + }) + await ready + // When the existing loader POSTs to the loopback ephemeral server. + const result = await new Promise<{ + readonly code: string | Buffer | undefined + readonly map: unknown + }>((resolve, reject) => { + const context = { + resourcePath, + getOptions: () => ({ + ...options, + themeFile: join(root, 'theme.json'), + watch: false, + defaultSheet: {}, + defaultClassMap: {}, + defaultFileMap: {}, + }), + addDependency: (_path: string) => {}, + async: () => { + const callback: ReturnType< + ThisParameterType['async'] + > = (error, code, map) => { + if (error) reject(error) + else resolve({ code, map }) + } + return callback + }, + } satisfies Pick< + ThisParameterType, + 'resourcePath' | 'getOptions' | 'addDependency' | 'async' + > + // Coordinator mode uses only these four real loader-context members. + Reflect.apply(loader, context, [Buffer.from(source)]) + }) + // Then absence becomes null; enabled maps survive HTTP byte-for-byte. + expect(result.code).toBe(generated.code) + expect(generated.map).toEqual(expect.any(String)) + expect(result.map).toBe(sourceMap ? generated.map : null) + } finally { + clearTimeout(readinessTimeout) + watcher.close() + coordinator?.close() + await flushCoordinatorWrites() + } + }) + }, + ) +}) From 1a606ef3f5e75a842df19950b76fb42e60acf670 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 9 Oct 2026 17:58:45 +0900 Subject: [PATCH 2/2] chore(changepack): release the no-map WASM fix as patch Refs #686 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .changepacks/changepack_log_wasm_skip_source_map.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changepacks/changepack_log_wasm_skip_source_map.json diff --git a/.changepacks/changepack_log_wasm_skip_source_map.json b/.changepacks/changepack_log_wasm_skip_source_map.json new file mode 100644 index 000000000..48c846453 --- /dev/null +++ b/.changepacks/changepack_log_wasm_skip_source_map.json @@ -0,0 +1,8 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch", + "packages/next-plugin/package.json": "Patch" + }, + "note": "Skip source-map generation in the codeExtractWithoutSourceMap WASM export while preserving extracted code, CSS and dependencies. Add real WASM and Next loader regressions for map-enabled and map-disabled extraction. No new build errors.", + "date": "2026-10-09T00:00:00.000Z" +}