2024년 5월 10일 작업일지

7 min read

터보팩 Tree shaking PR

어제 하던 작업에 이어서 로그를 추가한 뒤 돌려봤다.

        "./_/_interop_require_wildcard": Conditional(
            [
                (
                    "import",
                    Result(
                        "./esm/_interop_require_wildcard.js",
                    ),
                ),
                (
                    "default",
                    Result(
                        "./cjs/_interop_require_wildcard.cjs",
                    ),
                ),
            ],
        ),
[turbo/crates/turbopack-core/src/resolve/mod.rs:2262] handle_exports_imports_field(package_path, package_json_path, options,
            exports_field, &path, conditions, unspecified_conditions,
            query).await? = ResolveResult {
    primary: {
        RequestKey {
            request: Some(
                "./_/_interop_require_wildcard",
            ),
            conditions: {},
        }: Source(
            FileSource {
                path: FileSystemPath {
                    fs: DiskFileSystem {
                        name: "project",
                        root: "/Users/kdy1/projects/app-playground",
                    },
                    path: "node_modules/.pnpm/@swc+helpers@0.5.11/node_modules/@swc/helpers/cjs/_interop_require_wildcard.cjs",
                },
                query: "",
            },
        ),
    },
    affecting_sources: [
        FileSource {
            path: FileSystemPath {
                fs: DiskFileSystem {
                    name: "project",
                    root: "/Users/kdy1/projects/app-playground",
                },
                path: "node_modules/.pnpm/@swc+helpers@0.5.11/node_modules/@swc/helpers/package.json",
            },
            query: "",
        },
    ],
}

./_/_interop_require_wildcard가 포함된 로그 메시지들이다. 문제가 없어보여서 다른 부분을 먼저 보기로 했다.

const _interop_require_default = __turbopack_require__("[project]/node_modules/.pnpm/@swc+helpers@0.5.11/node_modules/@swc/helpers/cjs/_interop_require_default.cjs [ssr] (ecmascript) <facade>");
const _modernbrowserslisttarget = /*#__PURE__*/ _interop_require_default._(__turbopack_require__("[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/shared/lib/modern-browserslist-target.js [ssr] (ecmascript) <facade>"));

생성된 코드를 봤는데, 뭔가 문제가 있었다.

"[project]/node_modules/.pnpm/@swc+helpers@0.5.11/node_modules/@swc/helpers/cjs/_interop_require_default.cjs [ssr] (ecmascript) <facade>": (({ 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, x: __turbopack_external_require__, y: __turbopack_external_import__ }) => (() => {
"use strict";

__turbopack_esm__({});
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f40$swc$2b$helpers$40$0$2e$5$2e$11$2f$node_modules$2f40$swc$2f$helpers$2f$cjs$2f$_interop_require_default$2e$cjs__$5b$ssr$5d$__$28$ecmascript$29$__$3c$module__evaluation$3e$__ = __turbopack_import__("[project]/node_modules/.pnpm/@swc+helpers@0.5.11/node_modules/@swc/helpers/cjs/_interop_require_default.cjs [ssr] (ecmascript) <module evaluation>");
"__TURBOPACK__ecmascript__hoisting__location__";
;
;

})()),

<facade><exports>를 reexport 하지 않는 게 문제인 것 같았다.

                    // We can't use quote! as `with` is not standard yet
                    let chunk_prop = create_turbopack_part_id_assert(PartId::ModuleEvaluation);

                    module
                        .body
                        .push(ModuleItem::ModuleDecl(ModuleDecl::ExportAll(ExportAll {
                            span: DUMMY_SP,
                            src: Box::new(TURBOPACK_PART_IMPORT_SOURCE.into()),
                            type_only: false,
                            with: Some(Box::new(chunk_prop)),
                        })));

                    // We can't use quote! as `with` is not standard yet
                    let chunk_prop = create_turbopack_part_id_assert(PartId::Exports);

                    module
                        .body
                        .push(ModuleItem::ModuleDecl(ModuleDecl::ExportAll(ExportAll {
                            span: DUMMY_SP,
                            src: Box::new(TURBOPACK_PART_IMPORT_SOURCE.into()),
                            type_only: false,
                            with: Some(Box::new(chunk_prop)),
                        })));

그래서 관련된 코드를 봤더니 export * from "<exports>"는 정상적으로 생기고 있었다. 그래서 잠깐 고민하다가 CommonJS 모듈에 대해서는 module splitting을 하지 않게 바꾸기로 결정했다.


#[turbo_tasks::value(shared, serialization = "none", eq = "manual")]
pub(crate) enum SplitResult {
    Ok {
        asset_ident: Vc<AssetIdent>,

        /// `u32` is a index to `modules`.
        #[turbo_tasks(trace_ignore)]
        entrypoints: FxHashMap<Key, u32>,

        #[turbo_tasks(debug_ignore, trace_ignore)]
        modules: Vec<Vc<ParseResult>>,

        #[turbo_tasks(trace_ignore)]
        deps: FxHashMap<u32, Vec<u32>>,
    },
    Failed {
        parse_result: Vc<ParseResult>,
    },
}

이를 위해, SplitResult의 타입 정의를 위처럼 변경했다.

            // If the script file is a common js file, we cannot split the module
            if cjs_finder::contains_cjs(program) {
                return Ok(SplitResult::Failed {
                    parse_result: parsed,
                }
                .cell());
            }

그리고 common js 모듈을 탐지하는 로직을 넣었다. 그러고나서 로그를 보다보니까 SplitResult::Failed 인 경우 의존성이 로딩이 안 되는 것 같았다.




        let split_data = split_module(self.full_module).await?;

        let analyze = analyze(self.full_module, self.part).await?;

        let (deps, entrypoints) = match &*split_data {
            SplitResult::Ok {
                deps, entrypoints, ..
            } => (deps, entrypoints),
            SplitResult::Failed { .. } => return Ok(analyze.references),
        };

그래서 관련된 코드를 변경했다. 원래는 SplitResult::Failed { .. } 브랜치에서 Ok(Vc::cell(vec![]))를 반환했었다.

 ✓ Compiled /_error in 443ms
 ⨯ ReferenceError: RouteKind is not defined
    at /Users/kdy1/projects/app-playground/.next/server/chunks/ssr/node_modules__pnpm_0cbf07._.js:71:350
    at [project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <internal part 3> (/Users/kdy1/projects/app-playground/.next/server/chunks/ssr/node_modules__pnpm_0cbf07._.js:73:3)

그랬더니 에러 메시지가 달라졌다. 저 파일을 보면

"[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <internal part 3>": (({ 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, x: __turbopack_external_require__, y: __turbopack_external_import__ }) => (() => {
"use strict";

__turbopack_esm__({});
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f$file$2b2e2e2b$nextpack$2b$tarballs$2b$next$2e$tar_react$2d$dom$40$18$2e$2$2e$0_react$40$18$2e$2$2e$0$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$future$2f$route$2d$kind$2e$js__$5b$ssr$5d$__$28$ecmascript$29$__$3c$module__evaluation$3e$__ = __turbopack_import__("[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <module evaluation>");
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f$file$2b2e2e2b$nextpack$2b$tarballs$2b$next$2e$tar_react$2d$dom$40$18$2e$2$2e$0_react$40$18$2e$2$2e$0$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$future$2f$route$2d$kind$2e$js__$5b$ssr$5d$__$28$ecmascript$29$__$3c$internal__part__2$3e$__ = __turbopack_import__("[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <internal part 2>");
"__TURBOPACK__ecmascript__hoisting__location__";
;
(function(RouteKind1) {
    /**
   * `PAGES` represents all the React pages that are under `pages/`.
   */ RouteKind1["PAGES"] = "PAGES";
    /**
   * `PAGES_API` represents all the API routes under `pages/api/`.
   */ RouteKind1["PAGES_API"] = "PAGES_API";
    /**
   * `APP_PAGE` represents all the React pages that are under `app/` with the
   * filename of `page.{j,t}s{,x}`.
   */ RouteKind1["APP_PAGE"] = "APP_PAGE";
    /**
   * `APP_ROUTE` represents all the API routes and metadata routes that are under `app/` with the
   * filename of `route.{j,t}s{,x}`.
   */ RouteKind1["APP_ROUTE"] = "APP_ROUTE";
})(__TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f$file$2b2e2e2b$nextpack$2b$tarballs$2b$next$2e$tar_react$2d$dom$40$18$2e$2$2e$0_react$40$18$2e$2$2e$0$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$future$2f$route$2d$kind$2e$js__$5b$ssr$5d$__$28$ecmascript$29$__$3c$internal__part__2$3e$__["RouteKind"] || (RouteKind = {})); //# sourceMappingURL=route-kind.js.map

})()),
"[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <export RouteKind>": (({ 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, x: __turbopack_external_require__, y: __turbopack_external_import__ }) => (() => {
"use strict";

__turbopack_esm__({
    "RouteKind": ()=>__TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f$file$2b2e2e2b$nextpack$2b$tarballs$2b$next$2e$tar_react$2d$dom$40$18$2e$2$2e$0_react$40$18$2e$2$2e$0$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$future$2f$route$2d$kind$2e$js__$5b$ssr$5d$__$28$ecmascript$29$__$3c$internal__part__3$3e$__["RouteKind"]
});
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f$file$2b2e2e2b$nextpack$2b$tarballs$2b$next$2e$tar_react$2d$dom$40$18$2e$2$2e$0_react$40$18$2e$2$2e$0$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$future$2f$route$2d$kind$2e$js__$5b$ssr$5d$__$28$ecmascript$29$__$3c$module__evaluation$3e$__ = __turbopack_import__("[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <module evaluation>");
var __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f2e$pnpm$2f$file$2b2e2e2b$nextpack$2b$tarballs$2b$next$2e$tar_react$2d$dom$40$18$2e$2$2e$0_react$40$18$2e$2$2e$0$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$future$2f$route$2d$kind$2e$js__$5b$ssr$5d$__$28$ecmascript$29$__$3c$internal__part__3$3e$__ = __turbopack_import__("[project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <internal part 3>");
"__TURBOPACK__ecmascript__hoisting__location__";
;
;

})()),

생성된 코드를 보니까 타입스크립트 enum 초기화 코드중 일부가 Import 바인딩으로 바뀌어서 생성된 코드 같았다.


(function (RouteKind){})(RouteKind || RouteKind = {})

원본은 아마 위와 같을 것이다. Import binding을 치환하는 코드가 LHS에 바인딩이 있는 경우 (위 코드에서 RouteKind = {})를 처리 못하는 것 같아서 관련 코드를 찾아봤다.

imported__module 이 제일 특이한 코드니까 이것으로 검색했는데, 없길래 이것이 magic identifier 일 것이라고 판단하고 imported module 으로 검색해서 관련된 코드를 찾았다.

그리고 Expr::Ident를 처리하는 로직 옆에 SimpleAssignTarget::Ident 를 처리하는 로직을 넣었다.

근데 작동 안해서 여러가지를 시도했다. 그러다가 깨달은 게 있는데, ast_path 에선 BindingIdent를 건너뛴다던가 같은 건 하면 안 된다는 것...

그래서 코드가 살짝 더러워졌다. 한참 삽질하다가 어거지로 돌려보니까

 ○ Compiling /_error ...
 ✓ Compiled /_error in 511ms
 ⨯ TypeError: Cannot set property RouteKind of #<Object> which has only a getter
    at /Users/kdy1/projects/app-playground/.next/server/chunks/ssr/node_modules__pnpm_0cbf07._.js:71:672
    at [project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <internal part 3> (/Users/kdy1/projects/app-playground/.next/server/chunks/ssr/node_modules__pnpm_0cbf07._.js:73:3)

같은 에러가 떴다. 모듈의 모든 프로퍼티가 readonly라서 발생하는 이슈인데, 이러면 import 결과물을 변수에 저장했다가 사용해야한다.

처음엔 const 변수를 만들려고 했는데 중복 identifier 오류가 떠서 변수를 유일하게 만드는 대신 그냥 var을 써버렸다. 냄새가 나는 코드긴 하지만 어쨌든 나한테 중요한 건 tree shaking 패스 버그 수정이니까 그냥 무시했다.

근데 변수명이 겹쳐서 자기 자신을 참조하는 이슈가 있어서 __binding 을 붙였다.

근데 알고보니 insert 위치 문제였어서 insert_hoisted_stmt을 사용했다.

그런데 여전히 SimpleAssignTarget 을 위한 코드가 실행되지 않아서 필살기를 사용했다.

Debug info:
- An error occurred while generating the chunk item [project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <internal part 3>
- Execution of EcmascriptChunkItemContent::module_factory failed
- Execution of EcmascriptChunkItemContent::new failed
- A task panicked: explicit panic
    at [project]/node_modules/.pnpm/file+..+nextpack+tarballs+next.tar_react-dom@18.2.0_react@18.2.0/node_modules/next/dist/esm/server/future/route-kind.js [ssr] (ecmascript) <internal part 3> (/Users/kdy1/projects/app-playground/.next/server/chunks/ssr/node_modules__pnpm_0cbf07._.js:50:7)

하지만 통하지 않았다.

                        if let Some(binding) = &binding {
                            if binding.sym.contains("RouteKind") {
                                visitors.push(
                                    create_visitor!([], visit_mut_simple_assign_target(l: &mut SimpleAssignTarget) {
                                        dbg!(&l);
                                    }),
                                );
                            }
                        }

그래서 panic! 은 지우고 모듈 내의 모든 SimpleAssignTarget을 찍어보기로 했다.

[turbo/crates/turbopack-ecmascript/src/references/esm/binding.rs:196:41] &l = Member(
    MemberExpr {
        span: 591..613#0,
        obj: Ident(
            Ident {
                span: 591..600#3,
                sym: "RouteKind",
                optional: false,
            },
        ),
        prop: Computed(
            ComputedPropName {
                span: 600..613#0,
                expr: Lit(
                    Str(
                        Str {
                            span: 601..612#0,
                            value: "APP_ROUTE",
                            raw: Some(
                                "\"APP_ROUTE\"",
                            ),
                        },
                    ),
                ),
            },
        ),
    },
)
[turbo/crates/turbopack-ecmascript/src/references/esm/binding.rs:196:41] &l = Ident(
    BindingIdent {
        id: Ident {
            span: 646..655#2,
            sym: "RouteKind",
            optional: false,
        },
        type_ann: None,
    },
)

값이 찍혔고, 모듈 안에 존재하긴 한다는 걸 알게 됐다. ApplyVisitors 의 로직을 뜯어보기로 했는데, 로직이 복잡해보여서 무지성으로 로그찍기로 했다.


    #[inline(never)]
    fn visit_if_required<N>(&mut self, n: &mut N, ast_path: &mut AstKindPath<AstParentKind>)
    where
        N: for<'aa> VisitMutWith<dyn VisitMut + Send + Sync + 'aa>
            + for<'aa, 'bb> VisitMutWithPath<ApplyVisitors<'aa, 'bb>>,
    {
        let mut index = self.index;
        let mut current_visitors = self.visitors.as_ref();
        while index < ast_path.len() {
            dbg!(index, ast_path.len());

            let current = index == ast_path.len() - 1;
            dbg!(current);
            let kind = ast_path[index];
            dbg!(kind);
            if let Some(visitors) = find_range(current_visitors, &kind, index) {
                // visitors contains all items that match kind at index. Some of them terminate
                // here, some need furth visiting. The terminating items are at the start due to
                // sorting of the list.
                index += 1;

                // skip items that terminate here
                let nested_visitors_start =
                    visitors.partition_point(|(path, _)| path.len() == index);
                dbg!(nested_visitors_start, visitors.len());
                if current {
                    // Potentially skip visiting this sub tree
                    if nested_visitors_start < visitors.len() {
                        n.visit_mut_children_with_path(
                            &mut ApplyVisitors {
                                // We only select visitors starting from `nested_visitors_start`
                                // which maintains the invariant.
                                visitors: Cow::Borrowed(&visitors[nested_visitors_start..]),
                                index,
                            },
                            ast_path,
                        );
                    }
                    dbg!(visitors[..nested_visitors_start].len());
                    for (_, visitor) in visitors[..nested_visitors_start].iter() {
                        n.visit_mut_with(&mut visitor.create());
                    }
                    return;
                } else {
                    // `current_visitors` has the invariant that is must not be empty.
                    // When it becomes empty, we must early exit
                    current_visitors = &visitors[nested_visitors_start..];
                    dbg!(current_visitors.is_empty());
                    if current_visitors.is_empty() {
                        // Nothing to do in this subtree, skip it
                        return;
                    }
                }
            } else {
                // Skip visiting this sub tree
                return;
            }
        }
        // Ast path is unchanged, just keep visiting
        n.visit_mut_children_with_path(self, ast_path);
    }

로그를 읽기는 어렵겠지만 코드만 보고 디버깅하는 것보단 값 찍어보고 어느 코드가 실행되는지 보는 것이 훨씬 쉽다. 이 작업은 피로도가 꽤 높은 작업이라 오늘은 여기까지...

SWC 플러그인 테스트 정리

터보팩 PR 작업을 오래 했더니 피곤해서 쉬운 작업들을 하기로 했다. next#64890 을 고치려고 플러그인 레포를 vscode에서 열었는데 테스트 폴더가 camelCase인 것이 마음에 들지 않았다. 그래서 잠시 머리도 식힐 겸 테스트 정리부터 해서 PR로 만들었다.

SWC relay 플러그인 Config파싱 개선

next#64890 을 고치려고 봤더니 Wasm 플러그인이 serde_json::Value를 사용해서 설정을 파싱하길래 이를 #[derive(Deserialize)] 를 사용하게 변경했다.

next-swc : 큰 앱에서 터지는 버그 수정

swc_common::SourceMap 의 설계 미스로 인해 next-swc 가 터지는 버그인데, 해결은 간단하다. swc::Compiler가 들고있는 Arc<swc_common::SourceMap>를 재사용하지 않으면 되는 것이라서 swc::Compiler 인스턴스를 공유하지 않도록 했다.