Repository navigation
[전수조사/H] 빌드 플러그인·WASM 바인딩 #686
Copy link
Copy link
Open
Labels
bugSomething isn't workingSomething isn't working
Description
vite/webpack/next/rsbuild/bun 플러그인과 WASM 바인딩의 문제들입니다. 확인 예: Bun이 생성한 CSS를 버림, Bun의 사전 검사가 alias·StyleX 파일을 건너뜀, Rsbuild가 오래된 CSS를 제공.
전체 계획과 결정 사항은 추적 이슈(#682)에 있습니다. 조사 기준 커밋: 77daad74 (현재 main d0b84255).
발견 항목
심각도: P0 = 흔한 사용에서 조용히 틀리거나 크래시, P1 = 흔한 패턴이 명시적 에러로 막히거나 드물게 조용히 틀림, P2 = 드문 경우·도구/문서 불일치, P3 = 있으면 좋음.
필수 (4)
- PLG-01 (P0, WRONG) Bun discards generated CSS during bundling
- 근거:
packages/bun-plugin/src/plugin.ts:74-80,127-138resolves every generated stylesheet into{ contents: '', loader: 'js' }. Bun 1.4.2 officially supports anonLoadresult withloader: 'css', and JavaScript CSS imports are combined into emitted.css; the plugin instead turns the import into an empty JS module. - 수정 방향: Return current sheet contents with
loader:'css'for Bun.build, while using a separate runtime/test behavior if runtime imports truly need an empty module. Add an end-to-endBun.buildassertion for the emitted CSS asset.
- 근거:
- PLG-02 (P0, CRASH) Bun's pre-parse gate skips every compatibility alias and StyleX
- 근거:
compat-detection-probe.ts:hasDevupUI(...,'@devup-ui/react')returnedfalsefor@emotion/styled,@emotion/react/jsx-runtime, classic Emotion css-prop pragma, and@stylexjs/stylex. DirectcodeExtracttransformed Emotion styled and StyleX successfully, proving the gate—not the extractor—drops them. Published placeholders/aliases then execute at runtime or remain unc… - 수정 방향: Make the cheap gate alias-aware (including native StyleX and JSX-runtime/css-prop needles), then parse only candidates. Pass the merged alias table to a gate API or perform the measured substring precheck in TypeScript.
- 근거:
- PLG-03 (P0, WRONG) Rsbuild serves stale CSS after same-length/shorter edits and across plugin instances
- 근거:
packages/rsbuild-plugin/src/plugin.ts:68declares module-globalglobalCss; line 315 updates it only whenglobalCss.length < css.length; line 199 returns the cached value.red→tanor deleting declarations cannot update it, and a second build in the same process inherits the first build's longest sheet. - 수정 방향: Remove the length heuristic/module global. Resolve CSS from
getCss(...)at CSS load time and reset build state at setup/build boundaries; compare content, not length.
- 근거:
- PLG-04 (P0, NONDET) Webpack and Rsbuild can snapshot shared CSS before extraction completes
- 근거: Webpack
css-loader.ts:5-8returnsgetCsswhenever the generated CSS module is first built; the only final hook (plugin.ts:311-321) writes a disk file after compilation and cannot rewrite emitted assets. Prewarm is conditional and only scanssrc/, excluding included libraries. Rsbuild returnsglobalCssfrom its CSS transform and has nogenerateBundleequivalent. Vite… - 수정 방향: Add a completion coordinator/final asset rewrite, or pre-extract the exact full compiled graph including dynamic and include modules before any CSS module is served.
- 근거: Webpack
권장 (26)
- INF-06 (P1, WRONG) codeExtractWithoutSourceMap generates a source map — 재검증 후 P0→P1: 출력은 정상, source map을 불필요하게 생성
- 근거: public wrapper at
bindings/devup-ui-wasm/src/lib.rs:572-596callsSourceMapMode::Generate; declaration names itcodeExtractWithoutSourceMap(pkg/index.d.ts:27). - 수정 방향: pass
SourceMapMode::Skipand add a JS-boundary assertion thatmapis absent.
- 근거: public wrapper at
- INF-07 (P1, WRONG) Resolver errors are silently converted to unresolved modules
- 근거:
call_module_resolveruses.ok()?for JS call/reflection/conversion (bindings/.../lib.rs:473-490). - 수정 방향: distinguish resolver-returned null from thrown/malformed results and propagate contextual JS errors.
- 근거:
- INF-08 (P1, CRASH) Rust panics can become opaque WASM traps
- 근거: no installed panic hook or catch boundary; optional
console_error_panic_hookis unused. - 수정 방향: remove reachable panic paths and install a documented panic hook; do not claim ordinary error conversion for panics.
- 근거: no installed panic hook or catch boundary; optional
- NXT-04 (P1, WRONG) CSS loader reuses one app's coordinator port for every app
- 근거:
css-loader.ts:23-30stores a singlecachedPort, not a map keyed by port-file path.css-port-cache.tsstarted two coordinators and observed{firstResult:"FIRST",secondResult:"FIRST",expectedSecond:"SECOND"}. - 수정 방향: Cache by canonical port-file path, or re-read the tiny port file per compilation with health validation.
- 근거:
- NXT-05 (P1, CRASH) Source loader cannot recover after coordinator restart
- 근거:
loader.ts:29,43-49permanently caches the first port for a path; connection errors atloader.ts:121go straight to the callback.loader-stale-port.tsrewrote the same port file after restart and observedECONNREFUSEDinstead of the second coordinator's output. - 수정 방향: On connect failure, evict the cached port, re-read/health-check the current file, and retry within a bounded deadline.
- 근거:
- NXT-06 (P1, ERR-gap) Coordinator HTTP clients can hang a build indefinitely
- 근거:
loader.ts:78-124andcss-loader.ts:33-66set no request/connect timeout, deadline, or abort handling. The coordinator's internalmaxWaitMscannot help if the process accepts a connection but never responds. - 수정 방향: Set bounded request deadlines, destroy timed-out sockets, and return a contextual loader error; retry only idempotent startup/restart failures.
- 근거:
- NXT-07 (P1, WRONG) One process-global coordinator makes two in-process apps mutually exclusive
- 근거:
coordinator.ts:172,350-359closes any previous server regardless of app/port path.two-coordinators.tsobserved first health200, then firstECONNREFUSEDafter starting the second while the second remained200. - 수정 방향: Registry coordinators by normalized project/dist key with reference-counted close; never replace an unrelated instance.
- 근거:
- NXT-09 (P1, WRONG) Deleted or renamed files can leave active global CSS forever
- 근거: startup imports old sheet/maps (
plugin.ts:240-250). Coordinator exposes only/health,/css, and/extract(coordinator.ts:396-663); there is no remove/invalidate operation or fresh-state rebuild in dev. A deleted file never posts its old filename again, so its stored atoms/globalCss remain. - 수정 방향: Track the live module set and delete a file's sheet/map contribution on invalidation/removal; validate persisted state against current files at startup.
- 근거: startup imports old sheet/maps (
- NXT-12 (P1, PERF) Production prewarm extracts unreachable files and can fail on dead source
- 근거:
prewarm.ts:58-64,73-80explicitly adds every graph source file, not just route-reachable files;plugin.ts:387-426synchronously reads/extracts each without a per-file reachability check or fallback.plugin.test.ts:1033-1082locks this behavior. - 수정 방향: Prewarm the proven compiled closure plus specifically discovered MDX/template/package dependencies; add an explicit opt-in for conservative whole-tree prewarm rather than making it the default.
- 근거:
- NXT-14 (P1, WRONG) Persisted sheet/class/file maps are restored non-transactionally
- 근거: one
tryimports sheet, then class map, then file map (plugin.ts:244-250). If class/file JSON is missing or corrupt after sheet import, the catch leaves a mixed in-memory state. Writes are separate files (coordinator.ts:573-578) and are not an atomic multi-file transaction. - 수정 방향: Read and parse every snapshot first, validate a shared generation/version, then import all; write via generation directories plus atomic pointer/rename.
- 근거: one
- NXT-15 (P1, WRONG) Coordinator completion fallbacks silently serve partial CSS
- 근거: incomplete buckets resolve after quiet time or
maxWaitMsand still return HTTP 200 (coordinator.ts:319-342); base completion similarly fails open (coordinator.ts:229-259). Tests explicitly expect partial CSS atcoordinator.test.ts:1380-1411,1544-1634. - 수정 방향: In production, fail the CSS loader/build with missing filenames instead of serving partial output. Keep a separately labeled dev-only fallback if necessary.
- 근거: incomplete buckets resolve after quiet time or
- NXT-19 (P1, WRONG) Turbopack does not transform MDX source files
- 근거: source rule
*.{tsx,ts,jsx,js,mjs}atplugin.ts:573excludes.mdx;prewarm.ts:7excludes.mdx. The extra exclusion string passed atplugin.ts:265does not add a loader rule. Devup components/compat APIs authored directly in MDX can therefore reach runtime placeholders. - 수정 방향: Add an MDX-aware post-transform path/rule proven against Next's MDX loader ordering, and include MDX in graph/prewarm discovery.
- 근거: source rule
- PLG-05 (P1, WRONG)
includepackage names are unsafe raw regular expressions- 근거:
createNodeModulesExcludeRegexinterpolates names without regex escaping. Probe:include:['foo.bar']incorrectly includednode_modules/fooXbar;include:['pkg']also includednode_modules/@scope/pkg;foo+barfailed to include the literal package. Windows and Bun/pnpm store paths otherwise worked. - 수정 방향: Parse the package segment after each
node_modules, compare exact normalized package names, or escape every regex token and anchor unscoped/scoped package boundaries separately. Precompute once per plugin.
- 근거:
- PLG-06 (P1, CRASH) Modern module extensions and MDX fall through untransformed
- 근거:
filter-resolver-probe.ts: Vite/webpack/Rsbuild and import-graph accept only ts/tsx/js/jsx/mjs; Bun additionally skips ordinary local.js. All omit.mts,.cts,.cjs, raw.mdx; import graph omits the same modern TS modules. Devup placeholders or aliased APIs remain in emitted code. - 수정 방향: Share one explicit extension set across transform filters, graph enumeration, resolver, and entry filtering. Add MDX integration after MDX compilation (not against raw MDX syntax).
- 근거:
- PLG-07 (P1, WRONG) Removed/renamed modules leave stale styles during development
- 근거: Vite
watchChangehandles onlydevup.json; webpack persists sheet/class/file maps but has no deletion hook; Rsbuild/Bun register no removal handling.codeExtractremoves/replaces styles only when called for the same filename; a deleted filename is never called again. - 수정 방향: Add an engine remove-file API, call it on delete/rename, invalidate the corresponding generated CSS module, and persist the pruned maps in webpack watch mode.
- 근거: Vite
- PLG-08 (P1, WRONG) Theme inheritance dependencies are not watched; deletion is ignored
- 근거:
loadDevupConfig*recursively readsextends, but plugins register/watch onlydevupFile. Vite hot hooks require the top-level file still to exist; webpack captures existence atapply; Rsbuild/Bun have no config watch hook. Editing a base theme or deletingdevup.jsonleaves old CSS/types/default theme. - 수정 방향: Return resolved config dependencies from the loader, register all of them, and treat deletion as an empty config followed by CSS/type regeneration and reload.
- 근거:
- PLG-10 (P1, NONDET) Rsbuild cold-start CSS directory creation races its first write
- 근거:
packages/rsbuild-plugin/src/plugin.ts:96-103startsmkdir(cssDir)andwriteFile(cssDir/devup-ui.css)in the samePromise.all. Vite has the same issue documented and fixed sequentially atvite plugin.ts:248-255. - 수정 방향: Await recursive mkdir before starting any write, matching Vite.
- 근거:
- PLG-11 (P1, WRONG) Resolver and graph ignore inherited tsconfig paths
- 근거: Probe fixture used
tsconfig.jsonextending a base config containingbaseUrl/paths;createModuleResolver('@base/value',...)was unresolved.readPathAliasesparses only the immediate JSON (import-graph.ts:867-912). Atom/collapse reach and build-time imported constants therefore diverge from the bundler. - 수정 방향: Resolve the tsconfig inheritance chain with cycle detection and TS-compatible merge semantics (or use the installed
get-tsconfig/resolver stack).
- 근거: Probe fixture used
- PLG-12 (P1, WRONG) Module resolver selects the wrong conditional-export branch
- 근거: Probe package exports
{ browser:'./browser.js', import:'./import.js' }; resolver returnedimport.js.conditionTargetuses fixedimport,module,default,require,nodeand has no client/server/development/react-server condition input (import-graph.ts:1007-1091). - 수정 방향: Accept bundler-provided conditions and preserve exports object order; preferably delegate to each bundler resolver rather than maintain a partial resolver.
- 근거: Probe package exports
- PLG-13 (P1, WRONG) Atom-hoist graph roots miss real source layouts
- 근거: Vite detects both
srcandappfor file seeding but atomHoist picks onlysourceDirs[0]; webpack and Rsbuild hardcode<cwd>/src. Root-levelapp/, projects with both roots, custom source roots, and included libraries are omitted. - 수정 방향: Build a graph over all bundler entries and their resolved closures, or accept configured source roots; never pick only the first existing conventional directory.
- 근거: Vite detects both
- PLG-18 (P1, WRONG) Relative options ignore bundler project roots/contexts
- 근거: defaults are resolved when plugin constructors run; webpack graph/loader use
process.cwd()rather thancompiler.context; Rsbuild usesprocess.cwd(); only Vite's resolver/scan usesconfig.root, while its config/data paths still do not. Monorepo configs launched from a parent read/write the wrongdevup.json/dfand resolve imports under the wrong project. - 수정 방향: Delay normalization until setup/configResolved and resolve every relative option against the bundler's resolved root/context; pass that root to loaders/resolvers explicitly.
- 근거: defaults are resolved when plugin constructors run; webpack graph/loader use
- PLG-19 (P1, ERR-gap) Config errors are either silent or recurse without a bound
- 근거: malformed JSON returns
{}with no diagnostic (load-config.ts:48-57), silently clearing theme output. Recursiveextendshas no visited set (86-139), soa→b→arecurses until stack exhaustion/promise growth. - 수정 방향: Return typed parse/load errors with file paths; track a canonical-path stack and report a readable cycle.
- 근거: malformed JSON returns
- PLG-20 (P1, WRONG) Bun ignores imported build-time dependency tracking
- 근거: Bun
loadSourceFilediscardscodeExtract().dependencies; Vite, webpack and Rsbuild register the same values with their watcher APIs. Changes to imported token/style modules can leave transformed output and CSS stale. - 수정 방향: Return/register dependency watch files using Bun's current plugin API, or ensure the imported modules are explicit graph dependencies that invalidate the transformed importer.
- 근거: Bun
- PLG-21 (P1, WRONG) Rsbuild's shared CSS cache group silently does nothing under default
splitChunks- 근거: injector initializes
optimizationbut only adds the group whensplitChunksis already an object (plugin.ts:227-250). Its unit test explicitly confirms missing/false remains undefined. The option docs promise one shared CSS chunk so atom hoisting deduplicates MPA entries. - 수정 방향: Compose a valid splitChunks object when absent; handle
falseexplicitly (error or documented opt-out) instead of silently disabling the promised behavior.
- 근거: injector initializes
- PLG-22 (P1, CRASH) Library-mode demo emits runtime placeholders without documenting the consumer requirement
- 근거: allowed
apps/vite-libbuild produceddist/index.jsimportingBox/Flex/Textand rendering them directly becauseDevupUI({extractCss:false})skips transform. It works only if every consuming app lists the published library ininclude; plugin READMEs do not document this library publishing/consuming contract. - 수정 방향: Document and test the two supported modes: precompiled library with emitted CSS, or intentionally uncompiled library requiring consumer
include. Consider emitting package metadata/helper config to make the latter automatic.
- 근거: allowed
- PLG-09 (P2, WRONG) Empty themes leave stale generated declaration files
- 근거: Vite
plugin.ts:232-243, webpackplugin.ts:124-132, and Rsbuildplugin.ts:81-91only writetheme.d.tswheninterfaceCodeis truthy; none removes or overwrites a previous declaration when tokens disappear. Bun always writes and is not affected. - 수정 방향: Always overwrite
theme.d.tswith the generated empty augmentation, or remove it atomically when no interface is needed.
- 근거: Vite
선택 (11)
- NXT-10 (P2, MISMATCH)
debugis a public option but does nothing under Turbopack- 근거:
DevupUIBasePluginOptionsexposesdebug(plugin-utils/shared.ts:96-107), but Turbo destructuring omits it (plugin.ts:134-145) and the loaded WASM functions omitsetDebug(plugin.ts:209-228). Webpack receives it via{...options}. - 수정 방향: Destructure
debug, include it in the setup cache key, and callsetDebug(debug ?? false)before imports/extraction.
- 근거:
- NXT-11 (P2, WRONG) Turbopack does not watch theme data files
- 근거: coordinator mode returns at
loader.ts:145-194after adding only the port file; theme/state dependencies are added only in non-coordinator mode (loader.ts:209-227).css-loader.ts:83-120likewise fetches coordinator CSS without addingthemeFile. Theme registration occurs only during config setup (plugin.ts:252-257). - 수정 방향: Add the resolved config and all
extendsfiles as loader dependencies; reload/register theme and invalidate generated CSS when they change.
- 근거: coordinator mode returns at
- NXT-13 (P2, PERF) Shared RSC/client modules are re-extracted in development
- 근거: dev has no prewarmed-output cache (
plugin.ts:371), and every POST invokes WASM (coordinator.ts:462-483). Next may compile a shared module separately in server and client graphs and does not guarantee order: https://nextjs.org/docs/app/guides/server-and-client-boundary - 수정 방향: Add a bounded content-hash transform cache in the coordinator and invalidate by source/options/dependency changes.
- 근거: dev has no prewarmed-output cache (
- NXT-18 (P2, WRONG) Graph/prewarm planning failures are silently downgraded
- 근거: the broad catch at
plugin.ts:352-359discards the exception and silently leaves identity canonical mapping, no atom hoist, and idle completion. No warning reaches Next's terminal/overlay. - 수정 방향: Catch only recognized optional-resolution failures; otherwise throw in production or emit a contextual warning naming the file/phase and disabled guarantees.
- 근거: the broad catch at
- NXT-20 (P2, WRONG) Valid ESM/CJS TypeScript module extensions bypass Turbopack extraction
- 근거:
plugin.ts:573andprewarm.ts:7omit.mts,.cts, and.cjs, although imported application modules can use those extensions. - 수정 방향: Extend rule, graph parser, and prewarm filters together; add fixtures for each extension to prevent coordinator waits on untransformable graph members.
- 근거:
- PLG-23 (P2, PERF) Atom-hoist setup reparses every source tree twice (webpack can do a third pass)
- 근거: plugin-utils explicitly supports one
buildStaticImportGraphshared by builders (import-graph.ts:80-116), but Vite/webpack/Rsbuild callbuildCanonicalMapthencomputeFileReachwithoutgraph, causing two full list/read/parse/resolve passes. Webpack thenlistSourceFiles+read+codeExtractprewarm. - 수정 방향: Build one graph and pass it to both consumers; reuse its sorted files/external imports for prewarm.
- 근거: plugin-utils explicitly supports one
- PLG-24 (P2, PERF) Bun performs quadratic, blocking stylesheet I/O
- 근거: every matching module synchronously serializes and writes the entire growing global sheet (
bun plugin.ts:92-107). With N modules, early rules are rewritten O(N) times and synchronous disk I/O blocks all module loading. - 수정 방향: Serve CSS from the loader and write once in an end/finalization hook; for runtime watch, debounce/coalesce revisions without synchronous writes.
- 근거: every matching module synchronously serializes and writes the entire growing global sheet (
- PLG-27 (P2, MISMATCH) Best-effort catches silently disable determinism/hoisting
- 근거: Vite seed and atom prepass, webpack graph/prewarm, and Rsbuild atom prepass use empty catches or comments only. A resolver/parser/I/O defect silently falls back to arrival-order IDs or disables requested
atomHoist, producing a successful but different build. - 수정 방향: Fail builds for requested atomHoist/setup invariants; for optional seeding fallback, emit one structured warning with the root cause and deterministic impact.
- 근거: Vite seed and atom prepass, webpack graph/prewarm, and Rsbuild atom prepass use empty catches or comments only. A resolver/parser/I/O defect silently falls back to arrival-order IDs or disables requested
- PLG-29 (P2, MISMATCH) Bun unconditionally enables debug output
- 근거:
packages/bun-plugin/src/plugin.ts:125callssetDebug(true)and exposes no debug option, unlike all sibling plugins whose default is false. This changes class naming/output size and makes Bun output differ for identical source. - 수정 방향: Add
debug?:booleandefault false and set it on every initialization.
- 근거:
- NXT-17 (P3, MAINT) Exit cleanup claims to flush async writes but cannot await them
- 근거:
plugin.ts:488-491calls synchronousclosefromprocess.on('exit');coordinator.ts:673-680fire-and-forgetsflushPendingWrites(). Node'sexitevent cannot keep the event loop alive for asynchronous work: https://nodejs.org/api/process.html#event-exit - 수정 방향: Await drain during a pre-exit lifecycle hook/build completion path; keep
exitcleanup synchronous and best-effort only.
- 근거:
- PLG-28 (P3, PERF)
createNodeModulesExcludeRegexis rebuilt for every transformed module- 근거: Vite line 530 and Rsbuild line 260 call the regex factory inside hot transform callbacks although
includeis immutable. - 수정 방향: Compile once when constructing/setuping the plugin (after fixing PLG-05).
- 근거: Vite line 530 and Rsbuild line 260 call the regex factory inside hot transform callbacks although
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
chore: merge main into deterministic numbering
test(vite-plugin): follow array and restore plugin contract
test(css): cover route unions and restored mixed atom placement
test(next-plugin): release injected loader namespaces between suites
fix(plugin-utils): opt in to compiled MDX graph discovery
fix(webpack-plugin): normalize loader entry resources before prewarm
test(vite-plugin): isolate watched config diagnostics
chore: record plugin CI recovery patch releases
chore: merge plugin-core into next coordinator
chore: merge plugin-core CI fixes into next coordinator
fix(plugin-utils): number MDX only for compiler-owning callers
fix(vite-plugin): prepare complete library CSS before optimization
test(bun-plugin): cover internal setup and source callbacks completely
docs(vite-plugin): describe complete aggregate CSS and located limits
81 remaining items
fix(plugin-utils): certify closed manifest frontiers before DAG reuse
fix(plugin-utils): reserve complete production identity unions once
fix(plugin-utils): define dormant nonphysical identity list policy
chore: merge #755 dormant manifest growth correction
chore: merge #755 production numbering and nonphysical primitives
fix(webpack-plugin): add dormant returned-config manifest registry
fix(plugin-utils): add dormant scoped extraction ID store
chore: merge #755 webpack production manifest certificate bridge
chore: merge #755 dormant scoped extraction ID store
fix(vite-plugin): scope production numbering and recover watch inputs
fix(vite-plugin): derive numbering-store errno from the host filesystem
fix(test): report every native coverage gap with exact counts
fix(vite-plugin): cover native numbering-store rewrite failures
fix(plugins): recover production landing preparation
fix(vite-plugin): omit proven no-op numbering candidates