Repository: vercel/next.js
Pull request: Turbopack: trace fs-extra calls

This is supported by https://github.com/vercel/nft/

Closes PACK-5575

Changed files:
filename: turbopack/crates/turbopack-ecmascript/src/analyzer/mod.rs
patch: @@ -1772,6 +1772,10 @@ impl JsValue {
                         "fs",
                         "The Node.js fs module: https://nodejs.org/api/fs.html",
                     ),
+                    WellKnownObjectKind::FsExtraModule | WellKnownObjectKind::FsExtraModuleDefault => (
+                        "fs-extra",
+                        "The Node.js fs-extra module: https://github.com/jprichardson/node-fs-extra",
+                    ),
                     WellKnownObjectKind::FsModulePromises => (
                         "fs/promises",
                         "The Node.js fs module: https://nodejs.org/api/fs.html#promises-api",
@@ -3450,6 +3454,8 @@ pub enum WellKnownObjectKind {
     FsModule,
     FsModuleDefault,
     FsModulePromises,
+    FsExtraModule,
+    FsExtraModuleDefault,
     UrlModule,
     UrlModuleDefault,
     ChildProcess,

filename: turbopack/crates/turbopack-ecmascript/src/analyzer/well_known.rs
patch: @@ -600,6 +600,9 @@ async fn well_known_object_member(
         WellKnownObjectKind::FsModule
         | WellKnownObjectKind::FsModuleDefault
         | WellKnownObjectKind::FsModulePromises => fs_module_member(kind, prop),
+        WellKnownObjectKind::FsExtraModule | WellKnownObjectKind::FsExtraModuleDefault => {
+            fs_extra_module_member(kind, prop)
+        }
         WellKnownObjectKind::UrlModule | WellKnownObjectKind::UrlModuleDefault => {
             url_module_member(kind, prop)
         }
@@ -693,6 +696,45 @@ fn fs_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue {
     )
 }
 
+fn fs_extra_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue {
+    if let Some(word) = prop.as_str() {
+        match (kind, word) {
+            // regular fs methods
+            (
+                ..,
+                "realpath" | "realpathSync" | "stat" | "statSync" | "existsSync"
+                | "createReadStream" | "exists" | "open" | "openSync" | "readFile" | "readFileSync",
+            ) => {
+                return JsValue::WellKnownFunction(WellKnownFunctionKind::FsReadMethod(
+                    word.into(),
+                ));
+            }
+            // fs-extra specific
+            (
+                ..,
+                "pathExists" | "pathExistsSync" | "readJson" | "readJSON" | "readJsonSync"
+                | "readJSONSync",
+            ) => {
+                return JsValue::WellKnownFunction(WellKnownFunctionKind::FsReadMethod(
+                    word.into(),
+                ));
+            }
+            (WellKnownObjectKind::FsExtraModule, "default") => {
+                return JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModuleDefault);
+            }
+            _ => {}
+        }
+    }
+    JsValue::unknown(
+        JsValue::member(
+            Box::new(JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModule)),
+            Box::new(prop),
+        ),
+        true,
+        "unsupported property on fs-extra module",
+    )
+}
+
 fn url_module_member(kind: WellKnownObjectKind, prop: JsValue) -> JsValue {
     match (kind, prop.as_str()) {
         (.., Some("pathToFileURL")) => {

filename: turbopack/crates/turbopack-ecmascript/src/utils.rs
patch: @@ -214,6 +214,7 @@ pub fn module_value_to_well_known_object(module_value: &ModuleValue) -> Option<J
         }
         "resolve-from" => JsValue::WellKnownFunction(WellKnownFunctionKind::NodeResolveFrom),
         "@grpc/proto-loader" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProtobufLoader),
+        "fs-extra" => JsValue::WellKnownObject(WellKnownObjectKind::FsExtraModule),
         _ => return None,
     })
 }

filename: turbopack/crates/turbopack-tracing/tests/node-file-trace/test/unit/asset-fs-extra/output.js
patch: @@ -1,43 +1,43 @@
 ;[
-  'node_modules/fs-extra/lib/copy/copy-sync.js',
-  'node_modules/fs-extra/lib/copy/copy.js',
-  'node_modules/fs-extra/lib/copy/index.js',
-  'node_modules/fs-extra/lib/empty/index.js',
-  'node_modules/fs-extra/lib/ensure/file.js',
-  'node_modules/fs-extra/lib/ensure/index.js',
-  'node_modules/fs-extra/lib/ensure/link.js',
-  'node_modules/fs-extra/lib/ensure/symlink-paths.js',
-  'node_modules/fs-extra/lib/ensure/symlink-type.js',
-  'node_modules/fs-extra/lib/ensure/symlink.js',
-  'node_modules/fs-extra/lib/fs/index.js',
-  'node_modules/fs-extra/lib/index.js',
-  'node_modules/fs-extra/lib/json/index.js',
-  'node_modules/fs-extra/lib/json/jsonfile.js',
-  'node_modules/fs-extra/lib/json/output-json-sync.js',
-  'node_modules/fs-extra/lib/json/output-json.js',
-  'node_modules/fs-extra/lib/mkdirs/index.js',
-  'node_modules/fs-extra/lib/mkdirs/make-dir.js',
-  'node_modules/fs-extra/lib/mkdirs/utils.js',
-  'node_modules/fs-extra/lib/move/index.js',
-  'node_modules/fs-extra/lib/move/move-sync.js',
-  'node_modules/fs-extra/lib/move/move.js',
-  'node_modules/fs-extra/lib/output-file/index.js',
-  'node_modules/fs-extra/lib/path-exists/index.js',
-  'node_modules/fs-extra/lib/remove/index.js',
-  'node_modules/fs-extra/lib/remove/rimraf.js',
-  'node_modules/fs-extra/lib/util/stat.js',
-  'node_modules/fs-extra/lib/util/utimes.js',
-  'node_modules/fs-extra/package.json',
-  'node_modules/graceful-fs/clone.js',
-  'node_modules/graceful-fs/graceful-fs.js',
-  'node_modules/graceful-fs/legacy-streams.js',
-  'node_modules/graceful-fs/package.json',
-  'node_modules/graceful-fs/polyfills.js',
-  'node_modules/jsonfile/index.js',
-  'node_modules/jsonfile/package.json',
-  'node_modules/jsonfile/utils.js',
-  'node_modules/universalify/index.js',
-  'node_modules/universalify/package.json',
+  // 'node_modules/fs-extra/lib/copy/copy-sync.js',
+  // 'node_modules/fs-extra/lib/copy/copy.js',
+  // 'node_modules/fs-extra/lib/copy/index.js',
+  // 'node_modules/fs-extra/lib/empty/index.js',
+  // 'node_modules/fs-extra/lib/ensure/file.js',
+  // 'node_modules/fs-extra/lib/ensure/index.js',
+  // 'node_modules/fs-extra/lib/ensure/link.js',
+  // 'node_modules/fs-extra/lib/ensure/symlink-paths.js',
+  // 'node_modules/fs-extra/lib/ensure/symlink-type.js',
+  // 'node_modules/fs-extra/lib/ensure/symlink.js',
+  // 'node_modules/fs-extra/lib/fs/index.js',
+  // 'node_modules/fs-extra/lib/index.js',
+  // 'node_modules/fs-extra/lib/json/index.js',
+  // 'node_modules/fs-extra/lib/json/jsonfile.js',
+  // 'node_modules/fs-extra/lib/json/output-json-sync.js',
+  // 'node_modules/fs-extra/lib/json/output-json.js',
+  // 'node_modules/fs-extra/lib/mkdirs/index.js',
+  // 'node_modules/fs-extra/lib/mkdirs/make-dir.js',
+  // 'node_modules/fs-extra/lib/mkdirs/utils.js',
+  // 'node_modules/fs-extra/lib/move/index.js',
+  // 'node_modules/fs-extra/lib/move/move-sync.js',
+  // 'node_modules/fs-extra/lib/move/move.js',
+  // 'node_modules/fs-extra/lib/output-file/index.js',
+  // 'node_modules/fs-extra/lib/path-exists/index.js',
+  // 'node_modules/fs-extra/lib/remove/index.js',
+  // 'node_modules/fs-extra/lib/remove/rimraf.js',
+  // 'node_modules/fs-extra/lib/util/stat.js',
+  // 'node_modules/fs-extra/lib/util/utimes.js',
+  // 'node_modules/fs-extra/package.json',
+  // 'node_modules/graceful-fs/clone.js',
+  // 'node_modules/graceful-fs/graceful-fs.js',
+  // 'node_modules/graceful-fs/legacy-streams.js',
+  // 'node_modules/graceful-fs/package.json',
+  // 'node_modules/graceful-fs/polyfills.js',
+  // 'node_modules/jsonfile/index.js',
+  // 'node_modules/jsonfile/package.json',
+  // 'node_modules/jsonfile/utils.js',
+  // 'node_modules/universalify/index.js',
+  // 'node_modules/universalify/package.json',
   'package.json',
   'test/unit/asset-fs-extra/asset1.txt',
   'test/unit/asset-fs-extra/asset2.json',

filename: turbopack/crates/turbopack-tracing/tests/unit.rs
patch: @@ -44,12 +44,13 @@ static ALLOC: turbo_tasks_malloc::TurboMalloc = turbo_tasks_malloc::TurboMalloc;
 // TODO fix failures
 #[rstest]
 #[case::amd_disable("amd-disable")]
-// #[case::array_emission("array-emission")]
+#[case::array_emission("array-emission")]
 #[case::array_holes("array-holes")]
+// Ternary currently becomes Unknown as opposed to Alternatives when the condition isn't static
 // #[case::asset_conditional("asset-conditional")]
 #[case::asset_fs_array_expr("asset-fs-array-expr")]
 #[case::asset_fs_array_expr_node_prefix("asset-fs-array-expr-node-prefix")]
-// #[case::asset_fs_extra("asset-fs-extra")]
+#[case::asset_fs_extra("asset-fs-extra")]
 #[case::asset_fs_inline_path_babel("asset-fs-inline-path-babel")]
 #[case::asset_fs_inline_path_enc_es("asset-fs-inline-path-enc-es")]
 #[case::asset_fs_inline_path_enc_es_2("asset-fs-inline-path-enc-es-2")]
@@ -273,6 +274,8 @@ async fn to_list(assets: Vec<ResolvedVc<Box<dyn OutputAsset>>>) -> Result<Vec<Rc
 }
 
 static TRAILING_COMMA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r",[\s\n]*\]").unwrap());
+static LINE_COMMENTS_COMMA: LazyLock<Regex> =
+    LazyLock::new(|| Regex::new(r"(?m)^\s*//.*$").unwrap());
 
 fn node_file_trace(input_path: &str) -> Result<()> {
     let r = &mut {
@@ -305,8 +308,9 @@ fn node_file_trace(input_path: &str) -> Result<()> {
 
             let reference = std::fs::read_to_string(reference)?;
             // crude JS -> JSON conversion
-            let reference = TRAILING_COMMA
-                .replace(&reference, "]")
+            let reference = TRAILING_COMMA.replace(&reference, "]");
+            let reference = LINE_COMMENTS_COMMA
+                .replace_all(&reference, "")
                 .replace(";", "")
                 .replace('\'', "\"");
             let reference = serde_json::from_str::<Vec<String>>(&reference)?