2024년 5월 9일 작업일지

SWC 버그 수정 1

간단해보였는데 regression 때문에 생각보단 복잡했다. 최대한 regrssion을 줄인 뒤 리뷰 기다리기로 했다.

터보팩 Tree shaking PR

일단 uri_of_module 대신


const TURBOPACK_PART_IMPORT_SOURCE: &str = "__TURBOPACK_PART__";

를 임포트 경로로 쓰게 바꿨고, SplitResulturl_of_module: Atomasset_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/helperscjs/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까지만 했다. 메인테이너가 라이브러리 버그 수정해주면 의존성 업데이트하면서 이슈 닫을 생각이었다. 근데 메인테이너 분이 빠르게 이슈 잡아주셔서 오늘 마무리할 수 있었다.