2024년 5월 9일 작업일지
SWC 버그 수정 1
간단해보였는데 regression 때문에 생각보단 복잡했다. 최대한 regrssion을 줄인 뒤 리뷰 기다리기로 했다.
터보팩 Tree shaking PR
일단 uri_of_module
대신
const TURBOPACK_PART_IMPORT_SOURCE: &str = "__TURBOPACK_PART__";
를 임포트 경로로 쓰게 바꿨고, SplitResult
의 url_of_module: Atom
을 asset_ident: Vc<AssetIdent>
로 바꿨다.
당연히 __TURBOPACK__PART__
를 찾을 수 없다는 에러가 떴고, resolve
관련 로직에 로그 찍는 코드를 추가했다.
로그를 몇번 찍어보면서 처리 로직을 넣기 적당해보이는 곳을 찾아서 거기다가 __TURBOPACK_PART__
처리 코드를 넣었다.
그런데 turbopack-core
에 넣는 것보다는 turbopack
에 넣는 게 맞는 것 같아서 위치를 바꿨다. 근데 여기서 EcmascriptModuleAsset
을 가져오는 게 쉽지 않아보여서 아예 접근을 다르게 하기로 했다.
__TURBOPACK_PART__
를 경로로 가지는 Reference 는 EsmAssetReference
였기에 이 타입이 ModuleReference
로 바뀌는 부분을 패치해줬다.
결과는 반쯤 성공적이었다. 이제 서로 다른 모듈의 Module splitting 결과물이 섞이는 버그는 더 이상 발생하지 않기에 panic!
이나 bail!
이 발생하지 않았고, 대신 자바스크립트 실행 결과물로 exception이 발생했다.
<module evaluation>
모듈에서 exception이 떴는데, 보니까 module splitting 패스의 전역 변수 처리에 버그가 있는 것 같았다.
터지는 코드를 보면 exports
가 정의되지 않았다는 것을 알 수 있다.
저 파일의 원본을 찾기 위해 runtime.js
파일을 vscode를 이용해서 찾아봤는데 존재하지 않아서 runtime.ts
로 찾아보니까 있었다.
import RefreshRuntime from 'react-refresh/runtime'
import RefreshHelpers from './internal/helpers'
export type RefreshRuntimeGlobals = {
$RefreshReg$: (type: unknown, id: string) => void
$RefreshSig$: () => (type: unknown) => unknown
$RefreshInterceptModuleExecution$: (moduleId: string) => () => void
$RefreshHelpers$: typeof RefreshHelpers
}
declare const self: Window & RefreshRuntimeGlobals
// Hook into ReactDOM initialization
RefreshRuntime.injectIntoGlobalHook(self)
// Register global helpers
self.$RefreshHelpers$ = RefreshHelpers
// Register a helper for module execution interception
self.$RefreshInterceptModuleExecution$ = function (webpackModuleId) {
var prevRefreshReg = self.$RefreshReg$
var prevRefreshSig = self.$RefreshSig$
self.$RefreshReg$ = function (type, id) {
RefreshRuntime.register(type, webpackModuleId + ' ' + id)
}
self.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform
// Modeled after `useEffect` cleanup pattern:
// https://react.dev/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed
return function () {
self.$RefreshReg$ = prevRefreshReg
self.$RefreshSig$ = prevRefreshSig
}
}
ESM이고, exports
에 __esModule
속성을 추가하는 건 common js 패스가 하는 일이다. dist
폴더가 있길래 보니까 runtime.js
가 있었고, 내용물은
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const runtime_1 = __importDefault(require("react-refresh/runtime"));
const helpers_1 = __importDefault(require("./internal/helpers"));
// Hook into ReactDOM initialization
runtime_1.default.injectIntoGlobalHook(self);
// Register global helpers
self.$RefreshHelpers$ = helpers_1.default;
// Register a helper for module execution interception
self.$RefreshInterceptModuleExecution$ = function (webpackModuleId) {
var prevRefreshReg = self.$RefreshReg$;
var prevRefreshSig = self.$RefreshSig$;
self.$RefreshReg$ = function (type, id) {
runtime_1.default.register(type, webpackModuleId + ' ' + id);
};
self.$RefreshSig$ = runtime_1.default.createSignatureFunctionForTransform;
// Modeled after `useEffect` cleanup pattern:
// https://react.dev/learn/synchronizing-with-effects#step-3-add-cleanup-if-needed
return function () {
self.$RefreshReg$ = prevRefreshReg;
self.$RefreshSig$ = prevRefreshSig;
};
};
//# sourceMappingURL=runtime.js.map
이었다. 이 파일은 CJS 모듈에서 암시적으로 정의되는 exports
라는 변수에 의존하는 코드이고, 소스 뷰에서 컴파일된 모듈을 보니까 내용물이
"[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/compiled/@next/react-refresh-utils/dist/runtime.js [client] (ecmascript) <module evaluation>": (({ r: __turbopack_require__, f: __turbopack_module_context__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, n: __turbopack_export_namespace__, c: __turbopack_cache__, M: __turbopack_modules__, l: __turbopack_load__, j: __turbopack_dynamic__, P: __turbopack_resolve_absolute_path__, U: __turbopack_relative_url__, R: __turbopack_resolve_module_id_path__, g: global, __dirname, k: __turbopack_refresh__ }) => (() => {
"use strict";
__turbopack_esm__({
"__importDefault": ()=>__importDefault,
"runtime_1": ()=>runtime_1
});
"module evaluation";
"use strict";
var __importDefault = this && this.__importDefault || function(mod) {
return mod && mod.__esModule ? mod : {
"default": mod
};
};
Object.defineProperty(exports, "__esModule", {
value: true
});
const runtime_1 = __importDefault(__turbopack_require__("[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/compiled/react-refresh/runtime.js [client] (ecmascript) <facade>"));
// Hook into ReactDOM initialization
runtime_1.default.injectIntoGlobalHook(self);
;
;
})()),
이었다. 글로벌 변수에 관련된 유닛 테스트를 만들어둔 게 있었어서 그 테스트는 제대로 도는지 다시 한번 확인했는데, 안 돌았다.
only: SyntaxContext
를 통해 top-level 바인딩만 처리하도록 변경한 게 문제라는 걸 깨닫고 unresolved 변수들도 처리할 수 있게 only: [SyntaxContext; 2]
로 바꾸고 넘기는 인자를 [top_level_ctxt, unresolved_ctxt]
로 바꾸었다.
그러니까 유닛 테스트에서 뜨는 에러가 바뀌었는데, turbopack-node/js/src/ipc/index.ts
를 제대로 처리 못하는 것 같아서 유닛 테스트를 추가했다.
...
테스트하니까 나온 그래프인데 어차피 못 알아볼 것을 알아서 그냥 무시하고 코드를 비교해서 createIpc
이슈를 고쳤다.
ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
decl: Decl::Fn(f),
..
}))
| ModuleItem::Stmt(Stmt::Decl(Decl::Fn(f))) => {
let id = ItemId::Item {
index,
kind: ItemIdItemKind::Normal,
};
ids.push(id.clone());
let vars = ids_used_by(&f.function, [unresolved_ctxt, top_level_ctxt]);
let var_decls = {
let mut v = IndexSet::with_capacity_and_hasher(1, Default::default());
v.insert(f.ident.to_id());
v
};
items.insert(
id,
ItemData {
is_hoisted: true,
eventual_read_vars: vars.read,
eventual_write_vars: vars.write,
// 아랫줄 추가
write_vars: var_decls.clone(),
var_decls,
content: ModuleItem::Stmt(Stmt::Decl(Decl::Fn(f.clone()))),
..Default::default()
},
);
}
write_vars
가 올바르지 않은 값을 가지는 것이 문제의 원인이었다. 유닛 테스트에서 뜨는 에러는 바뀌었는데 유닛 테스트 직접 디버깅은 힘들 것 같아서 다시 app-playground
를 이용해서 디버깅했다.
이상한 파싱 실패가 뜨길래 로그 메시지를 추가했고, Program::Script
가 파싱 에러로 처리되고 있는 걸 발견해서 clone
좀 하더라도 작동은 하게 코드를 수정했다.
✓ Compiled /_error in 563ms
⨯ TypeError: _interop_require_default._ is not a function
at /Users/kdy1/projects/app-playground/.next/server/chunks/ssr/node_modules__pnpm_6dc888._.js:424:74
at [project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/shared/lib/constants.js [ssr] (ecmascript) <module evaluation> (/Users/kdy1/projects/app-playground/.next/server/chunks/ssr/node_modules__pnpm_6dc888._.js:636:3)
그러고 나니까 오류가 바뀌었다. 읽어보니까 @swc/helpers
의 cjs/interop_require_default
에서 _
를 가져오지 못했다는 것이었는데, 왜 cjs를 쓰는지는 모르겠지만 cjs 임포트가 실패하는 이유는 알 것 같았다.
생성된 코드를 확인하니 확실해졌는데, 이해 안 되는 점이 하나 있었다. 분명 임포트 대상은 CJS 모듈인데 ESM Export 바인딩이 생성됐다는 점이었다.
"use strict";
exports._ = exports._interop_require_default = _interop_require_default;
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
- 원본 코드
그래서 원본 코드를 봤고, ESM export는 없는 게 맞았다. 즉, 둘 중 하나다.
CJS 파일을 렉싱해서 ESM export를 만들어냈다
package.json
의 값을 읽어와서 ESM export를 만들어냈다.
시간도 시간이고 배고파서 집중 안 되기도 해서 오늘 작업은 여기서 중단했다.
SWC 유니코드 버그 디버깅
unicode_id_start
의 버그라 메인테이너 CC까지만 했다. 메인테이너가 라이브러리 버그 수정해주면 의존성 업데이트하면서 이슈 닫을 생각이었다. 근데 메인테이너 분이 빠르게 이슈 잡아주셔서 오늘 마무리할 수 있었다.