diff --git a/Cargo.lock b/Cargo.lock index c567aa7..70597ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,7 +666,7 @@ dependencies = [ "once_cell", "serde", "serde_json", - "thiserror", + "thiserror 1.0.67", ] [[package]] @@ -1563,7 +1563,7 @@ dependencies = [ "swc_ecma_parser", "swc_ecma_visit", "temp-dir", - "thiserror", + "thiserror 2.0.0", "tokio", ] @@ -1659,7 +1659,7 @@ dependencies = [ "miette-derive", "owo-colors", "textwrap", - "thiserror", + "thiserror 1.0.67", "unicode-width", ] @@ -2378,6 +2378,7 @@ version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ + "indexmap", "itoa", "memchr", "ryu", @@ -3533,7 +3534,7 @@ dependencies = [ "swc_atoms", "swc_common", "swc_ecma_ast", - "thiserror", + "thiserror 1.0.67", ] [[package]] @@ -3597,7 +3598,16 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.67", +] + +[[package]] +name = "thiserror" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15291287e9bff1bc6f9ff3409ed9af665bec7a5fc8ac079ea96be07bca0e2668" +dependencies = [ + "thiserror-impl 2.0.0", ] [[package]] @@ -3611,6 +3621,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thiserror-impl" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "time" version = "0.3.36" @@ -4197,7 +4218,7 @@ dependencies = [ "pbkdf2", "rand", "sha1", - "thiserror", + "thiserror 1.0.67", "time", "zeroize", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index c278387..f11a6c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ [workspace.dependencies] anyhow = "1.0.92" serde = { version = "1.0.213", features = ["derive"] } -serde_json = "1.0.132" +serde_json = { version = "1.0.132", features = ["preserve_order"] } swc = "4.0.0" swc_common = { version = "3.0.0", features = ["concurrent"] } swc_ecma_ast = "3.0.0" diff --git a/fn_build/Cargo.toml b/fn_build/Cargo.toml index acebb42..cc111bd 100644 --- a/fn_build/Cargo.toml +++ b/fn_build/Cargo.toml @@ -8,14 +8,14 @@ repository = { workspace = true } [dependencies] anyhow = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } swc = { workspace = true } swc_common = { workspace = true } swc_ecma_ast = { workspace = true } swc_ecma_parser = { workspace = true } swc_ecma_visit = { workspace = true } -thiserror = "1.0.67" +thiserror = "2.0.0" tokio = { workspace = true } [dev-dependencies] -serde_json = { workspace = true } temp-dir = { workspace = true } diff --git a/fn_build/fixtures/swc/nodejs/js/http_route/.fixture/parse.json b/fn_build/fixtures/swc/nodejs/js/http_route/.fixture/parse.json index 9993c07..a549040 100644 --- a/fn_build/fixtures/swc/nodejs/js/http_route/.fixture/parse.json +++ b/fn_build/fixtures/swc/nodejs/js/http_route/.fixture/parse.json @@ -1,6 +1,9 @@ -[ - { - "imports": [], - "path": "routes/data/lambda.js" - } -] +{ + "dependencies": "unused", + "sources": [ + { + "imports": [], + "path": "routes/data/lambda.js" + } + ] +} diff --git a/fn_build/fixtures/swc/nodejs/js/relative_import/.fixture/parse.json b/fn_build/fixtures/swc/nodejs/js/relative_import/.fixture/parse.json index 7eb1bad..424373e 100644 --- a/fn_build/fixtures/swc/nodejs/js/relative_import/.fixture/parse.json +++ b/fn_build/fixtures/swc/nodejs/js/relative_import/.fixture/parse.json @@ -1,14 +1,17 @@ -[ - { - "imports": [], - "path": "lib/data.js" - }, - { - "imports": [ - { - "relativeSource": "lib/data.js" - } - ], - "path": "routes/data/lambda.js" - } -] +{ + "dependencies": "unused", + "sources": [ + { + "imports": [], + "path": "lib/data.js" + }, + { + "imports": [ + { + "relativeSource": "lib/data.js" + } + ], + "path": "routes/data/lambda.js" + } + ] +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/build_debug.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/build_debug.json new file mode 100644 index 0000000..688babb --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/build_debug.json @@ -0,0 +1,6 @@ +[ + { + "path": "routes/data/lambda.js", + "result": "identical" + } +] diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/build_release.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/build_release.json new file mode 100644 index 0000000..a498d20 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/build_release.json @@ -0,0 +1,8 @@ +[ + { + "path": "routes/data/lambda.js", + "result": { + "content": "import{getData as o}from\"#lib/data.js\";export const GET=()=>{console.log(\"got\",o())};" + } + } +] diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/parse.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/parse.json new file mode 100644 index 0000000..03df652 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/parse.json @@ -0,0 +1,16 @@ +{ + "dependencies": "required", + "sources": [ + { + "imports": [ + { + "packageDependency": { + "package": "data-dep", + "subpath": null + } + } + ], + "path": "routes/data/lambda.js" + } + ] +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/spec.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/spec.json new file mode 100644 index 0000000..55388c5 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/.fixture/spec.json @@ -0,0 +1,3 @@ +{ + "entrypoint": "routes/data/lambda.js" +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/node_modules/data-dep/index.js b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/node_modules/data-dep/index.js new file mode 100644 index 0000000..55b65a9 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/node_modules/data-dep/index.js @@ -0,0 +1,3 @@ +export function getData() { + return [] +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/node_modules/data-dep/package.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/node_modules/data-dep/package.json new file mode 100644 index 0000000..07aec65 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/node_modules/data-dep/package.json @@ -0,0 +1,4 @@ +{ + "type": "module", + "main": "index.js" +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/package.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/package.json new file mode 100644 index 0000000..f2a278c --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "imports": { + "#lib/data.js": "data-dep" + } +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/routes/data/lambda.js b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/routes/data/lambda.js new file mode 100644 index 0000000..a594f34 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_dependency/routes/data/lambda.js @@ -0,0 +1,5 @@ +import {getData} from '#lib/data.js' + +export const GET = () => { + console.log('got', getData()) +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/build_debug.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/build_debug.json new file mode 100644 index 0000000..31377f8 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/build_debug.json @@ -0,0 +1,10 @@ +[ + { + "path": "lib/redis.js", + "result": "identical" + }, + { + "path": "routes/data/lambda.js", + "result": "identical" + } +] diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/build_release.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/build_release.json new file mode 100644 index 0000000..1ca4012 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/build_release.json @@ -0,0 +1,14 @@ +[ + { + "path": "lib/redis.js", + "result": { + "content": "let t=[];export function getData(){return t}" + } + }, + { + "path": "routes/data/lambda.js", + "result": { + "content": "import{getData as o}from\"#lib/data.js\";export const GET=()=>{console.log(\"got\",o())};" + } + } +] diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/parse.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/parse.json new file mode 100644 index 0000000..15fe605 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/parse.json @@ -0,0 +1,17 @@ +{ + "dependencies": "unused", + "sources": [ + { + "imports": [], + "path": "lib/redis.js" + }, + { + "imports": [ + { + "relativeSource": "lib/redis.js" + } + ], + "path": "routes/data/lambda.js" + } + ] +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/spec.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/spec.json new file mode 100644 index 0000000..55388c5 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/.fixture/spec.json @@ -0,0 +1,3 @@ +{ + "entrypoint": "routes/data/lambda.js" +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/lib/redis.js b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/lib/redis.js new file mode 100644 index 0000000..b8f104f --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/lib/redis.js @@ -0,0 +1,5 @@ +const empty = [] + +export function getData() { + return empty +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/package.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/package.json new file mode 100644 index 0000000..fcb69bb --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "imports": { + "#lib/data.js": "./lib/redis.js" + } +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/routes/data/lambda.js b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/routes/data/lambda.js new file mode 100644 index 0000000..a594f34 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/explicit_source/routes/data/lambda.js @@ -0,0 +1,5 @@ +import {getData} from '#lib/data.js' + +export const GET = () => { + console.log('got', getData()) +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/build_debug.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/build_debug.json new file mode 100644 index 0000000..add0dad --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/build_debug.json @@ -0,0 +1,14 @@ +[ + { + "path": "data/raw.js", + "result": "identical" + }, + { + "path": "data/abstraction/orm.js", + "result": "identical" + }, + { + "path": "routes/data/lambda.js", + "result": "identical" + } +] diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/build_release.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/build_release.json new file mode 100644 index 0000000..693186e --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/build_release.json @@ -0,0 +1,20 @@ +[ + { + "path": "data/raw.js", + "result": { + "content": "let t=[];export function getData(){return t}" + } + }, + { + "path": "data/abstraction/orm.js", + "result": { + "content": "let t=[[[]],[]];export function getComplexData(){return t}" + } + }, + { + "path": "routes/data/lambda.js", + "result": { + "content": "import{getData as o}from\"#lib/data/raw.js\";import{getComplexData as t}from\"#lib/data/abstraction/orm.js\";export const GET=()=>{console.log(\"got\",o()),console.log(\"getting\",t())};" + } + } +] diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/parse.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/parse.json new file mode 100644 index 0000000..09bbbc6 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/parse.json @@ -0,0 +1,24 @@ +{ + "dependencies": "unused", + "sources": [ + { + "imports": [], + "path": "data/raw.js" + }, + { + "imports": [], + "path": "data/abstraction/orm.js" + }, + { + "imports": [ + { + "relativeSource": "data/raw.js" + }, + { + "relativeSource": "data/abstraction/orm.js" + } + ], + "path": "routes/data/lambda.js" + } + ] +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/spec.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/spec.json new file mode 100644 index 0000000..55388c5 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/.fixture/spec.json @@ -0,0 +1,3 @@ +{ + "entrypoint": "routes/data/lambda.js" +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/data/abstraction/orm.js b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/data/abstraction/orm.js new file mode 100644 index 0000000..d6c0cf1 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/data/abstraction/orm.js @@ -0,0 +1,5 @@ +const architecturalDiagrams = [[[]], []] + +export function getComplexData() { + return architecturalDiagrams +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/data/raw.js b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/data/raw.js new file mode 100644 index 0000000..b8f104f --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/data/raw.js @@ -0,0 +1,5 @@ +const empty = [] + +export function getData() { + return empty +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/package.json b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/package.json new file mode 100644 index 0000000..57f8379 --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/package.json @@ -0,0 +1,7 @@ +{ + "description": "mapping a concatenation wildcard subpath import", + "type": "module", + "imports": { + "#lib/data/*": "./data/*" + } +} diff --git a/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/routes/data/lambda.js b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/routes/data/lambda.js new file mode 100644 index 0000000..da83fcd --- /dev/null +++ b/fn_build/fixtures/swc/nodejs/js/subpath_import/wildcard_concat/routes/data/lambda.js @@ -0,0 +1,7 @@ +import {getData} from '#lib/data/raw.js' +import {getComplexData} from '#lib/data/abstraction/orm.js' + +export const GET = () => { + console.log('got', getData()) + console.log('getting', getComplexData()) +} diff --git a/fn_build/src/build_test.rs b/fn_build/src/build_test.rs index d7cfb78..5cdedff 100644 --- a/fn_build/src/build_test.rs +++ b/fn_build/src/build_test.rs @@ -1,6 +1,8 @@ use crate::build_fn; use crate::result::FnBuildError; -use crate::spec::{BuildMode, FnBuildSpec}; +use crate::runtime::node::NodeConfig; +use crate::runtime::Runtime; +use crate::spec::{BuildMode, FnBuildSpec, FnParseSpec}; use std::env; use std::path::PathBuf; use temp_dir::TempDir; @@ -10,12 +12,15 @@ async fn build_fn_errors_for_invalid_extension() { let build_dir = TempDir::new().unwrap(); for entrypoint in &["README", "README.md"] { let build_spec = FnBuildSpec { - entrypoint: PathBuf::from(entrypoint), + function: FnParseSpec { + entrypoint: PathBuf::from(entrypoint), + project_dir: env::current_dir() + .unwrap() + .join("fixtures/swc/nodejs/js/http_route"), + runtime: Runtime::Node(NodeConfig::default()), + }, mode: BuildMode::Debug, output: build_dir.path().to_path_buf(), - project_dir: env::current_dir() - .unwrap() - .join("fixtures/swc/nodejs/js/http_route"), }; match build_fn(build_spec).await { Err(FnBuildError::InvalidFileType) => {} diff --git a/fn_build/src/lib.rs b/fn_build/src/lib.rs index 88484a8..4444109 100644 --- a/fn_build/src/lib.rs +++ b/fn_build/src/lib.rs @@ -1,5 +1,6 @@ mod paths; mod result; +mod runtime; mod spec; mod swc; @@ -15,18 +16,18 @@ mod paths_test; #[cfg(test)] mod testing; -use crate::result::{FnBuild, FnBuildError, FnBuildResult, FnSources}; +use crate::result::{FnBuild, FnBuildError, FnBuildResult, FnManifest}; use crate::spec::{FnBuildSpec, FnParseSpec}; use crate::swc::{build_js_fn, parse_js_fn}; pub async fn build_fn(build_spec: FnBuildSpec) -> FnBuildResult { - debug_assert!(build_spec.entrypoint.is_relative()); - debug_assert!(build_spec.entrypoint.parent().is_some()); + debug_assert!(build_spec.function.entrypoint.is_relative()); + debug_assert!(build_spec.function.entrypoint.parent().is_some()); debug_assert!(build_spec.output.is_absolute()); debug_assert!(build_spec.output.is_dir()); - debug_assert!(build_spec.project_dir.is_absolute()); - debug_assert!(build_spec.project_dir.is_dir()); - match build_spec.entrypoint.extension() { + debug_assert!(build_spec.function.project_dir.is_absolute()); + debug_assert!(build_spec.function.project_dir.is_dir()); + match build_spec.function.entrypoint.extension() { None => Err(FnBuildError::InvalidFileType), Some(extension) => match extension.to_string_lossy().as_ref() { "js" | "mjs" => build_js_fn(build_spec).await, @@ -37,7 +38,7 @@ pub async fn build_fn(build_spec: FnBuildSpec) -> FnBuildResult { } } -pub async fn parse_fn(parse_spec: FnParseSpec) -> FnBuildResult { +pub async fn parse_fn(parse_spec: FnParseSpec) -> FnBuildResult { debug_assert!(parse_spec.entrypoint.is_relative()); debug_assert!(parse_spec.entrypoint.parent().is_some()); debug_assert!(parse_spec.project_dir.is_absolute()); diff --git a/fn_build/src/parse_test.rs b/fn_build/src/parse_test.rs index 592517f..724b428 100644 --- a/fn_build/src/parse_test.rs +++ b/fn_build/src/parse_test.rs @@ -1,3 +1,5 @@ +use crate::runtime::node::NodeConfig; +use crate::runtime::Runtime; use crate::spec::FnParseSpec; use crate::{parse_fn, FnBuildError}; use std::env; @@ -11,6 +13,7 @@ async fn parse_fn_errors_for_invalid_extension() { project_dir: env::current_dir() .unwrap() .join("fixtures/swc/nodejs/js/http_route"), + runtime: Runtime::Node(NodeConfig::default()), }; match parse_fn(parse_spec).await { Err(FnBuildError::InvalidFileType) => {} diff --git a/fn_build/src/paths.rs b/fn_build/src/paths.rs index f82340f..68343ee 100644 --- a/fn_build/src/paths.rs +++ b/fn_build/src/paths.rs @@ -1,41 +1,42 @@ -use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR}; +pub mod join { + use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR}; -pub fn join_relative_path(base: &Path, relative: &Path) -> PathBuf { - debug_assert!(base.is_relative()); - debug_assert!(relative.to_string_lossy().starts_with('.')); - rewrite_current_and_parent_path_segments( - match base.file_name() { - None => base, - Some(_) => base.parent().unwrap(), - } - .join(relative), - ) -} + pub fn directory_and_relative_file_path(base: &Path, relative: &Path) -> PathBuf { + debug_assert!(base.is_dir()); + debug_assert!(relative.to_string_lossy().starts_with('.')); + rewrite_current_and_parent_path_segments(base.join(relative)) + } -/// Removes `.` and `..` segments from paths, rewriting `..` to the parent directory -/// This fn returns Ok(None) if it's a noop (path does not have any current or parent segments) -pub fn rewrite_current_and_parent_path_segments(p: PathBuf) -> PathBuf { - let mut stack: Vec = Vec::new(); - let mut changed = false; - for path_component_os_str in &p { - let path_component = path_component_os_str.to_string_lossy(); - match path_component.as_ref() { - "." => changed = true, - ".." => { - if stack.pop().is_none() { - panic!( - "rewrote fs path to outside project for {}", - p.to_string_lossy() - ); + /// Joins file paths and rewrites `.` and `..` segments from result. + pub fn file_and_relative_file_paths(base: &Path, relative: &Path) -> PathBuf { + // debug_assert!(base.is_file()); + debug_assert!(relative.to_string_lossy().starts_with('.')); + rewrite_current_and_parent_path_segments(base.parent().unwrap().join(relative)) + } + + fn rewrite_current_and_parent_path_segments(p: PathBuf) -> PathBuf { + let mut stack: Vec = Vec::new(); + let mut changed = false; + for path_component_os_str in &p { + let path_component = path_component_os_str.to_string_lossy(); + match path_component.as_ref() { + "." => changed = true, + ".." => { + if stack.pop().is_none() { + panic!( + "rewrote fs path to outside project for {}", + p.to_string_lossy() + ); + } + changed = true; } - changed = true; + _ => stack.push(path_component.to_string()), } - _ => stack.push(path_component.to_string()), } - } - if changed { - PathBuf::from(stack.join(MAIN_SEPARATOR_STR)) - } else { - p + if changed { + PathBuf::from(stack.join(MAIN_SEPARATOR_STR)) + } else { + p + } } } diff --git a/fn_build/src/paths_test.rs b/fn_build/src/paths_test.rs index 47bf20f..cac44aa 100644 --- a/fn_build/src/paths_test.rs +++ b/fn_build/src/paths_test.rs @@ -1,31 +1,50 @@ -use crate::paths::rewrite_current_and_parent_path_segments; +use crate::paths::join; use std::path::PathBuf; #[test] -fn test_rewrite_current_and_parent_path_segments_with_absolute_path() { - let result = rewrite_current_and_parent_path_segments(PathBuf::from( - "/user/project/routes/data/../../src/data.js", - )); - assert_eq!(result, PathBuf::from("/user/project/src/data.js")); +fn test_join_directory_and_relative_file_path_joins_dir_with_sibling() { + let base = PathBuf::from("fixtures/swc/nodejs/js/subpath_import/explicit_source"); + let relative = PathBuf::from("./lib/redis.js"); + let result = join::directory_and_relative_file_path(&base, &relative); + assert_eq!( + result, + PathBuf::from("fixtures/swc/nodejs/js/subpath_import/explicit_source/lib/redis.js") + ); + assert!(result.is_file()); } #[test] -fn test_rewrite_current_and_parent_path_segments_with_relative_path() { - let result = - rewrite_current_and_parent_path_segments(PathBuf::from("routes/data/../../src/data.js")); - assert_eq!(result, PathBuf::from("src/data.js")); +fn test_join_directory_and_relative_file_path_joins_dir_with_ancestor() { + let base = PathBuf::from("fixtures/swc/nodejs/js/subpath_import/explicit_source"); + let relative = PathBuf::from("../explicit_source/lib/redis.js"); + let result = join::directory_and_relative_file_path(&base, &relative); + assert_eq!( + result, + PathBuf::from("fixtures/swc/nodejs/js/subpath_import/explicit_source/lib/redis.js") + ); + assert!(result.is_file()); } #[test] -fn test_rewrite_current_and_parent_path_segments_with_current_path_segment() { - let result = rewrite_current_and_parent_path_segments(PathBuf::from( - "/user/project/routes/data/.././lambda.js", - )); - assert_eq!(result, PathBuf::from("/user/project/routes/lambda.js")); +fn test_join_file_and_relative_file_joins_rel_path_with_ancestor() { + let base = PathBuf::from("fixtures/swc/nodejs/js/relative_import/routes/data/lambda.js"); + let relative = PathBuf::from("../../lib/data.js"); + let result = join::file_and_relative_file_paths(&base, &relative); + assert_eq!( + join::file_and_relative_file_paths(&base, &relative), + PathBuf::from("fixtures/swc/nodejs/js/relative_import/lib/data.js") + ); + assert!(result.is_file()); } #[test] -fn test_rewrite_current_and_parent_path_segments_for_noop() { - let result = rewrite_current_and_parent_path_segments(PathBuf::from("routes/data/lambda.js")); - assert_eq!(result, PathBuf::from("routes/data/lambda.js")); +fn test_join_file_and_relative_file_joins_rel_path_with_sibling() { + let base = PathBuf::from("fixtures/swc/nodejs/js/relative_import/routes/data/lambda.js"); + let relative = PathBuf::from("../.././lib/data.js"); + let result = join::file_and_relative_file_paths(&base, &relative); + assert_eq!( + join::file_and_relative_file_paths(&base, &relative), + PathBuf::from("fixtures/swc/nodejs/js/relative_import/lib/data.js") + ); + assert!(result.is_file()); } diff --git a/fn_build/src/result.rs b/fn_build/src/result.rs index 8505531..8db970b 100644 --- a/fn_build/src/result.rs +++ b/fn_build/src/result.rs @@ -24,13 +24,25 @@ pub struct FnSource { pub path: PathBuf, } -pub type FnSources = Vec; +#[derive(Clone, Deserialize)] +#[cfg_attr(test, serde(rename_all = "camelCase"))] +pub enum FnDependencies { + Required, + Unused, +} +#[derive(Clone, Deserialize)] +pub struct FnManifest { + pub dependencies: FnDependencies, + pub sources: Vec, +} + +#[derive(Clone, Deserialize)] pub struct FnBuild { #[allow(unused)] - pub output: FnBuildOutput, + pub manifest: FnManifest, #[allow(unused)] - pub sources: FnSources, + pub output: FnBuildOutput, } #[derive(thiserror::Error, Debug)] diff --git a/fn_build/src/runtime/import_resolver.rs b/fn_build/src/runtime/import_resolver.rs new file mode 100644 index 0000000..e5d0bef --- /dev/null +++ b/fn_build/src/runtime/import_resolver.rs @@ -0,0 +1,6 @@ +use crate::result::ModuleImport; +use std::path::Path; + +pub trait ImportResolver: Send + Sync { + fn resolve(&self, project_dir: &Path, from: &Path, import: &str) -> ModuleImport; +} diff --git a/fn_build/src/runtime/mod.rs b/fn_build/src/runtime/mod.rs new file mode 100644 index 0000000..6700412 --- /dev/null +++ b/fn_build/src/runtime/mod.rs @@ -0,0 +1,9 @@ +pub use crate::runtime::import_resolver::ImportResolver; +use crate::runtime::node::NodeConfig; + +mod import_resolver; +pub mod node; + +pub enum Runtime { + Node(NodeConfig), +} diff --git a/fn_build/src/runtime/node/mod.rs b/fn_build/src/runtime/node/mod.rs new file mode 100644 index 0000000..0d1699e --- /dev/null +++ b/fn_build/src/runtime/node/mod.rs @@ -0,0 +1,11 @@ +pub use node_config::*; +pub use node_imports::*; + +mod node_config; +mod node_imports; + +#[cfg(test)] +mod node_config_test; + +#[cfg(test)] +mod node_imports_test; diff --git a/fn_build/src/runtime/node/node_config.rs b/fn_build/src/runtime/node/node_config.rs new file mode 100644 index 0000000..0313eb0 --- /dev/null +++ b/fn_build/src/runtime/node/node_config.rs @@ -0,0 +1,147 @@ +use serde_json::Value; +use std::path::Path; +use std::{fs, io}; + +#[derive(thiserror::Error, Debug)] +#[error("error initializing package.json: {0}")] +pub enum NodeConfigError { + IoError(#[from] io::Error), + JsonError(#[from] serde_json::Error), +} + +#[derive(Clone, Default)] +pub struct NodeConfig { + /// True if package.json's "type" is explicitly set to "module" + pub module_type: bool, + /// Subpath imports from package.json for these conditions and priority order: + /// - "node" + /// - "import" + /// - "module-sync" + /// - "default" + /// + /// Read spec at . + pub subpath_imports: NodeSubpathImports, +} + +impl NodeConfig { + pub fn parse_node_config(package_json: &str) -> Result { + Ok(Self::from(serde_json::from_str::(package_json)?)) + } + + pub fn read_node_config(path: &Path) -> Result { + let path = if path + .file_name() + .map(|filename| filename == "package.json") + .unwrap_or(false) + { + path.to_path_buf() + } else { + path.join("package.json") + }; + if path.is_file() { + Self::parse_node_config(fs::read_to_string(path)?.as_str()) + } else { + Ok(Default::default()) + } + } + + fn read_module_type(package_json: &Value) -> bool { + package_json["type"] + .as_str() + .map(|t| t == "module") + .unwrap_or(false) + } + + fn read_subpath_imports(package_json: &Value) -> NodeSubpathImports { + let mut result: NodeSubpathImports = Vec::new(); + if let Value::Object(imports) = &package_json["imports"] { + for (map_from, map_to) in imports { + if !is_valid_subpath_import_map_from_specifier(map_from) { + continue; + } + if let Some(map_to) = map_to.as_str() { + if is_valid_subpath_import_specifier(map_to) { + result.push(( + NodeSubpathImportSpecifier::from(map_from.as_str()), + vec![NodeSubpathImportSpecifier::from(map_to)], + )); + } + } else if let Some(map_to) = map_to.as_object() { + result.push(( + NodeSubpathImportSpecifier::from(map_from.as_str()), + Self::collect_subpath_import_conditions(map_to), + )); + } + } + } + result + } + + // todo rewrite to prioritize condition ordering instead of json object key order + // todo research why serde map iter is in reverse key order + // todo test mapping subpath imports to dependency packages + fn collect_subpath_import_conditions( + conditions_json: &serde_json::Map, + ) -> Vec { + let mut result = Vec::new(); + for (condition, map_to) in conditions_json.iter() { + match condition.as_ref() { + "node" | "import" | "module-sync" | "default" => { + if let Some(map_to) = map_to.as_str() { + if is_valid_subpath_import_specifier(map_to) { + result.push(NodeSubpathImportSpecifier::from(map_to)); + } + } else if let Some(map_to) = map_to.as_object() { + result.append(&mut Self::collect_subpath_import_conditions(map_to)); + } + } + _ => {} + } + } + result + } +} + +impl From for NodeConfig { + fn from(package_json: Value) -> Self { + Self { + module_type: Self::read_module_type(&package_json), + subpath_imports: Self::read_subpath_imports(&package_json), + } + } +} + +fn is_valid_subpath_import_map_from_specifier(s: &str) -> bool { + s.starts_with('#') && is_valid_subpath_import_specifier(s) +} + +fn is_valid_subpath_import_specifier(s: &str) -> bool { + s.chars().filter(|c| *c == '*').count() < 2 +} + +pub type NodeSubpathImports = Vec<(NodeSubpathImportSpecifier, Vec)>; + +#[derive(Clone, Debug, PartialEq)] +pub enum NodeSubpathImportSpecifier { + Explicit(String), + Wildcard { + before: String, + after: Option, + }, +} + +impl From<&str> for NodeSubpathImportSpecifier { + fn from(specifier: &str) -> Self { + match specifier.split_once('*') { + None => NodeSubpathImportSpecifier::Explicit(specifier.to_string()), + Some((before, after)) => NodeSubpathImportSpecifier::Wildcard { + before: before.to_string(), + after: if after.is_empty() { + None + } else { + Some(after.to_string()) + }, + }, + } + } +} diff --git a/fn_build/src/runtime/node/node_config_test.rs b/fn_build/src/runtime/node/node_config_test.rs new file mode 100644 index 0000000..7ef8f54 --- /dev/null +++ b/fn_build/src/runtime/node/node_config_test.rs @@ -0,0 +1,212 @@ +use crate::runtime::node::{NodeConfig, NodeSubpathImportSpecifier}; + +#[test] +pub fn test_parse_node_config_parses_explicit_subpath_import() { + let node_config = NodeConfig::parse_node_config( + r##"{ + "imports": { + "#lib/data.js": "./lib/data.js" + } + }"##, + ) + .unwrap(); + assert_eq!(1, node_config.subpath_imports.len()); + assert_eq!( + NodeSubpathImportSpecifier::Explicit("#lib/data.js".to_string()), + node_config.subpath_imports.first().unwrap().0 + ); + assert_eq!( + &NodeSubpathImportSpecifier::Explicit("./lib/data.js".to_string()), + node_config + .subpath_imports + .first() + .unwrap() + .1 + .first() + .unwrap() + ); +} + +#[test] +pub fn test_parse_node_config_parses_wildcard_prefix_only_subpath_import() { + let node_config = NodeConfig::parse_node_config( + r##"{ + "imports": { + "#lib/*": "./lib/*" + } + }"##, + ) + .unwrap(); + assert_eq!(1, node_config.subpath_imports.len()); + assert_eq!( + NodeSubpathImportSpecifier::Wildcard { + before: "#lib/".to_string(), + after: None + }, + node_config.subpath_imports.first().unwrap().0 + ); + assert_eq!( + &NodeSubpathImportSpecifier::Wildcard { + before: "./lib/".to_string(), + after: None + }, + node_config + .subpath_imports + .first() + .unwrap() + .1 + .first() + .unwrap() + ); +} + +#[test] +pub fn test_parse_node_config_parses_wildcard_with_suffix_subpath_import() { + let node_config = NodeConfig::parse_node_config( + r##"{ + "imports": { + "#lib/*.js": "./lib/*.js" + } + }"##, + ) + .unwrap(); + assert_eq!(1, node_config.subpath_imports.len()); + assert_eq!( + NodeSubpathImportSpecifier::Wildcard { + before: "#lib/".to_string(), + after: Some(".js".to_string()), + }, + node_config.subpath_imports.first().unwrap().0 + ); + assert_eq!( + &NodeSubpathImportSpecifier::Wildcard { + before: "./lib/".to_string(), + after: Some(".js".to_string()), + }, + node_config + .subpath_imports + .first() + .unwrap() + .1 + .first() + .unwrap() + ); +} + +#[test] +pub fn test_parse_node_config_parses_conditional_subpath_imports() { + let node_config = NodeConfig::parse_node_config( + r##"{ + "imports": { + "#lib/*": { + "node": "./lib/node/*", + "default": "./lib/default/*" + } + } + }"##, + ) + .unwrap(); + assert_eq!(1, node_config.subpath_imports.len()); + assert_eq!( + NodeSubpathImportSpecifier::Wildcard { + before: "#lib/".to_string(), + after: None, + }, + node_config.subpath_imports.first().unwrap().0 + ); + assert_eq!( + &NodeSubpathImportSpecifier::Wildcard { + before: "./lib/node/".to_string(), + after: None, + }, + node_config + .subpath_imports + .first() + .unwrap() + .1 + .first() + .unwrap() + ); + assert_eq!( + &NodeSubpathImportSpecifier::Wildcard { + before: "./lib/default/".to_string(), + after: None, + }, + node_config + .subpath_imports + .first() + .unwrap() + .1 + .iter() + .nth(1) + .unwrap() + ); +} + +#[test] +pub fn test_parse_node_config_parses_nesting_conditional_subpath_imports() { + let node_config = NodeConfig::parse_node_config( + r##"{ + "imports": { + "#lib/*": { + "node": { + "import": "./lib/node/import/*", + "module-sync": "./lib/node/module-sync/*" + }, + "default": "./lib/default/*" + } + } + }"##, + ) + .unwrap(); + assert_eq!(1, node_config.subpath_imports.len()); + assert_eq!( + NodeSubpathImportSpecifier::Wildcard { + before: "#lib/".to_string(), + after: None, + }, + node_config.subpath_imports.first().unwrap().0 + ); + assert_eq!(3, node_config.subpath_imports.first().unwrap().1.len()); + assert_eq!( + &NodeSubpathImportSpecifier::Wildcard { + before: "./lib/node/import/".to_string(), + after: None, + }, + node_config + .subpath_imports + .first() + .unwrap() + .1 + .first() + .unwrap() + ); + assert_eq!( + &NodeSubpathImportSpecifier::Wildcard { + before: "./lib/node/module-sync/".to_string(), + after: None, + }, + node_config + .subpath_imports + .first() + .unwrap() + .1 + .iter() + .nth(1) + .unwrap() + ); + assert_eq!( + &NodeSubpathImportSpecifier::Wildcard { + before: "./lib/default/".to_string(), + after: None, + }, + node_config + .subpath_imports + .first() + .unwrap() + .1 + .iter() + .nth(2) + .unwrap() + ); +} diff --git a/fn_build/src/runtime/node/node_imports.rs b/fn_build/src/runtime/node/node_imports.rs new file mode 100644 index 0000000..4e74893 --- /dev/null +++ b/fn_build/src/runtime/node/node_imports.rs @@ -0,0 +1,115 @@ +use crate::paths::join; +use crate::result::ModuleImport; +use crate::runtime::import_resolver::ImportResolver; +use crate::runtime::node::{NodeConfig, NodeSubpathImportSpecifier}; +use std::path::{Path, PathBuf}; + +// todo nodejs subpath imports +// https://nodejs.org/api/packages.html#subpath-imports +// todo cross-check swc implementation +// https://github.com/swc-project/swc/blob/main/crates/swc_ecma_loader/src/resolvers/node.rs +pub struct NodeImportResolver<'a> { + pub node_config: &'a NodeConfig, +} + +impl NodeImportResolver<'_> { + fn resolve_relative_path( + &self, + project_dir: &Path, + from: &Path, + import: &str, + ) -> Option { + let maybe = join::file_and_relative_file_paths(from, &PathBuf::from(import)); + if project_dir.join(&maybe).is_file() { + Some(maybe) + } else { + None + } + } + + fn resolve_subpath_import(&self, project_dir: &Path, import: &str) -> Option { + for (map_from, map_tos) in &self.node_config.subpath_imports { + match map_from { + NodeSubpathImportSpecifier::Explicit(map_from) => { + if map_from == import { + for map_to in map_tos { + if let NodeSubpathImportSpecifier::Explicit(map_to) = map_to { + if map_to.starts_with('.') { + let maybe = join::directory_and_relative_file_path( + project_dir, + &PathBuf::from(map_to), + ); + if maybe.is_file() { + return Some(ModuleImport::RelativeSource( + maybe.strip_prefix(project_dir).unwrap().to_path_buf(), + )); + } + } else { + return Some(match map_to.split_once('/') { + None => ModuleImport::PackageDependency { + package: map_to.clone(), + subpath: None, + }, + Some(_) => todo!(), + }); + } + } + } + } + } + NodeSubpathImportSpecifier::Wildcard { + before: map_from_before_wc, + after: _, + } => { + let import_subpath = + if let Some(import_subpath) = import.strip_prefix(map_from_before_wc) { + import_subpath + } else { + continue; + }; + for map_to in map_tos { + match map_to { + NodeSubpathImportSpecifier::Explicit(_) => todo!(), + NodeSubpathImportSpecifier::Wildcard { + before: map_to_before_wc, + after: map_to_after_wc, + } => { + match map_to_after_wc { + None => { + let maybe = PathBuf::from( + // todo is map_to without a ./ prefix possible? + map_to_before_wc.strip_prefix("./").unwrap(), + ) + .join(import_subpath); + if project_dir.join(&maybe).is_file() { + return Some(ModuleImport::RelativeSource(maybe)); + } else { + continue; + } + } + Some(_) => todo!(), + } + } + }; + } + } + } + } + None + } +} + +impl ImportResolver for NodeImportResolver<'_> { + fn resolve(&self, project_dir: &Path, from: &Path, import: &str) -> ModuleImport { + if import.starts_with('.') { + if let Some(relative_path) = self.resolve_relative_path(project_dir, from, import) { + return ModuleImport::RelativeSource(relative_path); + } + } else if import.starts_with('#') { + if let Some(subpath_import) = self.resolve_subpath_import(project_dir, import) { + return subpath_import; + } + } + ModuleImport::Unknown(import.to_string()) + } +} diff --git a/fn_build/src/runtime/node/node_imports_test.rs b/fn_build/src/runtime/node/node_imports_test.rs new file mode 100644 index 0000000..d72b57a --- /dev/null +++ b/fn_build/src/runtime/node/node_imports_test.rs @@ -0,0 +1,82 @@ +use crate::result::ModuleImport; +use crate::runtime::node::{NodeConfig, NodeImportResolver}; +use crate::runtime::ImportResolver; +use std::path::PathBuf; + +#[test] +fn test_node_import_resolver_resolves_relative_source() { + match (NodeImportResolver { + node_config: &NodeConfig::default(), + }) + .resolve( + &PathBuf::from("fixtures/swc/nodejs/js/relative_import"), + &PathBuf::from("routes/data/lambda.js"), + "../../lib/data.js", + ) { + ModuleImport::RelativeSource(path) => assert_eq!(PathBuf::from("lib/data.js"), path), + _ => panic!(), + }; +} + +#[test] +fn test_node_import_resolver_resolves_relative_source_with_explicit_match_from_subpath_import() { + let project_dir = PathBuf::from("fixtures/swc/nodejs/js/subpath_import/explicit_source"); + match (NodeImportResolver { + node_config: &NodeConfig::read_node_config(&project_dir).unwrap(), + }) + .resolve( + &project_dir, + &PathBuf::from("routes/data/lambda.js"), + "#lib/data.js", + ) { + ModuleImport::RelativeSource(path) => assert_eq!(PathBuf::from("lib/redis.js"), path), + _ => panic!(), + }; +} + +#[test] +fn test_node_import_resolver_resolves_relative_source_with_explicit_dependency_from_subpath_import() +{ + let project_dir = PathBuf::from("fixtures/swc/nodejs/js/subpath_import/explicit_dependency"); + match (NodeImportResolver { + node_config: &NodeConfig::read_node_config(&project_dir).unwrap(), + }) + .resolve( + &project_dir, + &PathBuf::from("routes/data/lambda.js"), + "#lib/data.js", + ) { + ModuleImport::PackageDependency { package, subpath } => { + assert_eq!("data-dep", package); + assert!(subpath.is_none()); + } + _ => panic!(), + }; +} + +#[test] +fn test_node_import_resolver_resolves_wildcard_concatenation_match_from_subpath_import() { + let project_dir = PathBuf::from("fixtures/swc/nodejs/js/subpath_import/wildcard_concat"); + let import_resolver = NodeImportResolver { + node_config: &NodeConfig::read_node_config(&project_dir).unwrap(), + }; + match import_resolver.resolve( + &project_dir, + &PathBuf::from("routes/data/lambda.js"), + "#lib/data/raw.js", + ) { + ModuleImport::RelativeSource(path) => assert_eq!(PathBuf::from("data/raw.js"), path), + _ => panic!(), + }; + + match import_resolver.resolve( + &project_dir, + &PathBuf::from("routes/data/lambda.js"), + "#lib/data/abstraction/orm.js", + ) { + ModuleImport::RelativeSource(path) => { + assert_eq!(PathBuf::from("data/abstraction/orm.js"), path) + } + _ => panic!(), + }; +} diff --git a/fn_build/src/spec.rs b/fn_build/src/spec.rs index 3c3965c..5e296c6 100644 --- a/fn_build/src/spec.rs +++ b/fn_build/src/spec.rs @@ -1,3 +1,4 @@ +use crate::runtime::Runtime; use std::path::PathBuf; #[derive(Clone, Eq, Hash, PartialEq)] @@ -14,13 +15,13 @@ pub type FnBuildOutput = PathBuf; // } pub struct FnBuildSpec { - pub entrypoint: PathBuf, + pub function: FnParseSpec, pub mode: BuildMode, pub output: FnBuildOutput, - pub project_dir: PathBuf, } pub struct FnParseSpec { pub entrypoint: PathBuf, pub project_dir: PathBuf, + pub runtime: Runtime, } diff --git a/fn_build/src/swc/compiler.rs b/fn_build/src/swc/compiler.rs index c3255ac..55b6e81 100644 --- a/fn_build/src/swc/compiler.rs +++ b/fn_build/src/swc/compiler.rs @@ -1,5 +1,5 @@ use std::io; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::{Arc, Mutex}; use swc::config::IsModule; use swc::Compiler; @@ -31,8 +31,8 @@ pub enum CompileError { CompilerDiagnostics(Vec), #[error("compiler operation produced error: {0}")] OperationError(String), - #[error("error reading source file {0}: {1}")] - ReadError(PathBuf, io::Error), + #[error("reading source io error: {0}")] + ReadError(#[from] io::Error), } pub type CompileResult = Result; @@ -90,10 +90,7 @@ impl SwcCompiler { where F: FnOnce(&Compiler, &Handler, Arc) -> Result, { - let source_file = match self.source_map.load_file(p) { - Ok(source_file) => source_file, - Err(err) => return Err(CompileError::ReadError(p.to_path_buf(), err)), - }; + let source_file = self.source_map.load_file(p)?; self.with_compiler(|compiler, handler| f(compiler, handler, source_file)) } diff --git a/fn_build/src/swc/imports.rs b/fn_build/src/swc/imports.rs deleted file mode 100644 index ea25c62..0000000 --- a/fn_build/src/swc/imports.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::paths::join_relative_path; -use crate::result::ModuleImport; -use std::path::{Path, PathBuf}; - -pub trait ImportResolver: Send + Sync { - fn resolve(&self, project_dir: &Path, from: &Path, import: &str) -> ModuleImport; -} - -// todo nodejs subpath imports -// https://nodejs.org/api/packages.html#subpath-imports -// todo cross-check swc implementation -// https://github.com/swc-project/swc/blob/main/crates/swc_ecma_loader/src/resolvers/node.rs -pub struct NodeImportResolver {} - -impl ImportResolver for NodeImportResolver { - fn resolve(&self, project_dir: &Path, from: &Path, import: &str) -> ModuleImport { - if import.starts_with('.') { - let maybe = project_dir.join(join_relative_path(from, &PathBuf::from(import))); - if maybe.is_file() { - return ModuleImport::RelativeSource( - maybe.strip_prefix(project_dir).unwrap().to_path_buf(), - ); - } - println!("{}", maybe.to_string_lossy()); - } - ModuleImport::Unknown(import.to_string()) - } -} diff --git a/fn_build/src/swc/imports_test.rs b/fn_build/src/swc/imports_test.rs deleted file mode 100644 index 7d0e408..0000000 --- a/fn_build/src/swc/imports_test.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::result::ModuleImport; -use crate::swc::imports::{ImportResolver, NodeImportResolver}; -use std::fs; -use std::path::PathBuf; -use temp_dir::TempDir; - -#[test] -fn test_node_sibling_relative_import() { - let project_dir = TempDir::new().unwrap(); - fs::write(project_dir.child("api.js"), "").unwrap(); - fs::write(project_dir.child("data.js"), "").unwrap(); - - let resolver = NodeImportResolver {}; - let result = resolver.resolve(project_dir.path(), &PathBuf::from("api.js"), "./data.js"); - match result { - ModuleImport::RelativeSource(path) => assert_eq!(PathBuf::from("data.js"), path), - _ => panic!(), - }; -} - -#[test] -fn test_node_ancestor_relative_import() { - let project_dir = TempDir::new().unwrap(); - let api_path = project_dir.child("apis/api.js"); - fs::create_dir(api_path.parent().unwrap()).unwrap(); - fs::write(&api_path, "").unwrap(); - fs::write(project_dir.child("data.js"), "").unwrap(); - - let resolver = NodeImportResolver {}; - let result = resolver.resolve(project_dir.path(), &PathBuf::from("api.js"), "./data.js"); - match result { - ModuleImport::RelativeSource(path) => assert_eq!(PathBuf::from("data.js"), path), - _ => panic!(), - }; -} diff --git a/fn_build/src/swc/mod.rs b/fn_build/src/swc/mod.rs index 7e68268..9ec7b9b 100644 --- a/fn_build/src/swc/mod.rs +++ b/fn_build/src/swc/mod.rs @@ -1,19 +1,16 @@ -use crate::result::{FnBuild, FnBuildResult, FnSource, FnSources, ModuleImport}; +use crate::result::{FnBuild, FnBuildResult, FnDependencies, FnManifest, FnSource, ModuleImport}; +use crate::runtime::node::{NodeConfig, NodeImportResolver}; +use crate::runtime::{ImportResolver, Runtime}; use crate::spec::{BuildMode, FnBuildSpec, FnParseSpec}; use crate::swc::compiler::SwcCompiler; -use crate::swc::imports::{ImportResolver, NodeImportResolver}; use crate::swc::visitors::ImportVisitor; use std::fs; use std::path::Path; use swc_ecma_visit::FoldWith; mod compiler; -mod imports; mod visitors; -#[cfg(test)] -mod imports_test; - #[cfg(test)] mod swc_test; @@ -21,60 +18,85 @@ mod swc_test; mod visitors_test; pub async fn build_js_fn(build_spec: FnBuildSpec) -> FnBuildResult { - let sources = parse_js_fn(FnParseSpec { - entrypoint: build_spec.entrypoint.clone(), - project_dir: build_spec.project_dir.clone(), - }) - .await?; - for source in &sources { + let manifest = parse_js_fn_inner(&build_spec.function).await?; + for source in &manifest.sources { debug_assert!(source.path.is_relative()); let output_path = build_spec.output.join(&source.path); fs::create_dir_all(output_path.parent().unwrap()).expect("mkdir -p"); match build_spec.mode { BuildMode::Debug => { - fs::copy(build_spec.project_dir.join(&source.path), output_path).expect("cp"); + fs::copy( + build_spec.function.project_dir.join(&source.path), + output_path, + ) + .expect("cp"); } BuildMode::Release => { - let js_path = build_spec.project_dir.join(&source.path); + let js_path = build_spec.function.project_dir.join(&source.path); let minified_js = SwcCompiler::new().minify_js(&js_path).unwrap(); fs::write(output_path, minified_js).unwrap(); } } } Ok(FnBuild { - sources, + manifest, output: build_spec.output, }) } -pub async fn parse_js_fn(parse_spec: FnParseSpec) -> FnBuildResult { +pub async fn parse_js_fn(parse_spec: FnParseSpec) -> FnBuildResult { + parse_js_fn_inner(&parse_spec).await +} + +async fn parse_js_fn_inner(parse_spec: &FnParseSpec) -> FnBuildResult { + let Runtime::Node(node_config) = &parse_spec.runtime; let mut sources = Vec::new(); let compiler = SwcCompiler::new(); let entrypoint = parse_js( + node_config, compiler.clone(), &parse_spec.project_dir, &parse_spec.entrypoint, )?; + let mut requires_deps = false; for import in &entrypoint.imports { match import { + ModuleImport::PackageDependency { .. } => requires_deps = true, ModuleImport::RelativeSource(path) => { - sources.push(parse_js(compiler.clone(), &parse_spec.project_dir, path)?); + sources.push(parse_js( + node_config, + compiler.clone(), + &parse_spec.project_dir, + path, + )?); } - _ => panic!(), + ModuleImport::Unknown(_) => panic!(), } } - sources.push(entrypoint); - Ok(sources) + sources.insert(0, entrypoint); + Ok(FnManifest { + dependencies: if requires_deps { + FnDependencies::Required + } else { + FnDependencies::Unused + }, + sources, + }) } -fn parse_js(compiler: SwcCompiler, project_dir: &Path, source: &Path) -> FnBuildResult { +fn parse_js( + node_config: &NodeConfig, + compiler: SwcCompiler, + project_dir: &Path, + source: &Path, +) -> FnBuildResult { debug_assert!(project_dir.is_absolute()); debug_assert!(project_dir.is_dir()); debug_assert!(source.is_relative()); let module = compiler.parse_es_module(&project_dir.join(source)).unwrap(); let mut visitor = ImportVisitor::new(); module.fold_with(&mut visitor); - let import_resolver = NodeImportResolver {}; + let import_resolver = NodeImportResolver { node_config }; let imports = visitor .result() .into_iter() diff --git a/fn_build/src/swc/swc_test.rs b/fn_build/src/swc/swc_test.rs index a1b8609..3fb7e84 100644 --- a/fn_build/src/swc/swc_test.rs +++ b/fn_build/src/swc/swc_test.rs @@ -1,8 +1,10 @@ -use crate::result::{FnBuild, FnSources}; +use crate::result::{FnBuild, FnManifest}; +use crate::runtime::node::NodeConfig; +use crate::runtime::Runtime; use crate::spec::{FnBuildSpec, FnParseSpec}; use crate::swc::{build_js_fn, parse_js_fn}; use crate::testing::{run_fixtures, BuildProcess, BuildProcessResult}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; struct JavaScriptBuild {} @@ -12,16 +14,23 @@ impl BuildProcess for JavaScriptBuild { Box::pin(build_js_fn(build_spec)) } - fn parse(&self, parse_spec: FnParseSpec) -> BuildProcessResult { + fn parse(&self, parse_spec: FnParseSpec) -> BuildProcessResult { Box::pin(parse_js_fn(parse_spec)) } + + fn runtime_config(&self, project_dir: &Path) -> Runtime { + Runtime::Node(NodeConfig::read_node_config(&project_dir).unwrap()) + } } #[tokio::test] -pub async fn test_nodejs_js_fixtures() { +pub async fn test_node_js_fixtures() { run_fixtures( Arc::new(Box::new(JavaScriptBuild {})), - PathBuf::from("fixtures/swc/nodejs/js"), + vec![ + PathBuf::from("fixtures/swc/nodejs/js"), + PathBuf::from("fixtures/swc/nodejs/js/subpath_import"), + ], ) .await; } diff --git a/fn_build/src/testing.rs b/fn_build/src/testing.rs index 6e6ec61..b2bbf9c 100644 --- a/fn_build/src/testing.rs +++ b/fn_build/src/testing.rs @@ -1,5 +1,7 @@ -use crate::result::{FnBuild, FnBuildError, FnSources}; +use crate::result::{FnBuild, FnBuildError, FnManifest}; +use crate::runtime::Runtime; use crate::spec::{BuildMode, FnBuildSpec, FnParseSpec}; +use anyhow::anyhow; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::future::Future; @@ -47,24 +49,41 @@ pub type BuildProcessResult = Pin BuildProcessResult; - fn parse(&self, parse_spec: FnParseSpec) -> BuildProcessResult; + fn parse(&self, parse_spec: FnParseSpec) -> BuildProcessResult; + + fn runtime_config(&self, project_dir: &Path) -> Runtime; } fn collect_fixture_dirs(p: PathBuf) -> Vec { debug_assert!(p.is_relative()); let mut dirs = Vec::new(); - for dir_read_result in fs::read_dir(env::current_dir().unwrap().join(p)).unwrap() { + let abs_p = env::current_dir().unwrap().join(p); + debug_assert!(abs_p.is_dir()); + for dir_read_result in fs::read_dir(abs_p).unwrap() { let dir_read = dir_read_result.unwrap(); let dir_read_path = dir_read.path(); if dir_read_path.is_dir() { - dirs.push(dir_read_path); + if dir_read_path.join(".fixture").is_dir() { + if !dir_read_path + .file_name() + .unwrap() + .to_string_lossy() + .starts_with('_') + { + dirs.push(dir_read_path); + } + } } } dirs } -pub async fn run_fixtures(build_process: Arc>, fixture_root_dir: PathBuf) { - for dir in collect_fixture_dirs(fixture_root_dir) { +pub async fn run_fixtures(build_process: Arc>, fixture_roots: Vec) { + for dir in fixture_roots + .into_iter() + .flat_map(collect_fixture_dirs) + .collect::>() + { let test_fixture = TestFixture::new(build_process.clone(), dir); if let Err(err) = test_fixture.test().await { panic!("{err}"); @@ -99,8 +118,8 @@ impl TestFixture { if self.leak_build_dir { temp_build_dir.leak(); println!( - "{} {}", - self.fixture_dir.to_string_lossy(), + "fixture {} build dir is {}", + self.fixture_label(), build_dir_root.to_string_lossy() ); } @@ -120,10 +139,13 @@ impl TestFixture { async fn build(&self, build_dir: PathBuf, build_mode: BuildMode) -> Result<(), anyhow::Error> { self.build_process .build(FnBuildSpec { - entrypoint: self.spec.entrypoint.clone(), + function: FnParseSpec { + entrypoint: self.spec.entrypoint.clone(), + project_dir: self.fixture_dir.clone(), + runtime: self.build_process.runtime_config(&self.fixture_dir), + }, mode: build_mode, output: build_dir, - project_dir: self.fixture_dir.clone(), }) .await?; Ok(()) @@ -138,7 +160,8 @@ impl TestFixture { let built_file_path = build_dir.join(&build_file.path); let built_content = fs::read_to_string(&built_file_path).expect( format!( - "failed reading build output file {}", + "failed reading fixture {} build output file {}", + self.fixture_label(), built_file_path.to_string_lossy() ) .as_str(), @@ -149,7 +172,8 @@ impl TestFixture { let original_file_path = self.fixture_dir.join(&build_file.path); fs::read_to_string(&original_file_path).expect( format!( - "failed reading original file {}", + "failed reading fixture {} original file {}", + self.fixture_label(), original_file_path.to_string_lossy() ) .as_str(), @@ -159,8 +183,9 @@ impl TestFixture { assert_eq!( expected_content, built_content, - "{} did not match expected content in build dir {}", + "{} from fixture {} did not match expected content in build dir {}", build_file.path.to_string_lossy(), + self.fixture_label(), build_dir.to_string_lossy(), ); } @@ -168,24 +193,24 @@ impl TestFixture { } async fn expect_parse(&self) -> Result<(), anyhow::Error> { - let expected_sources = self.read_expected_parse_result(); - let result_sources = self + let expected_manifest = self.read_expected_parse_result(); + let result_manifest = self .build_process .parse(FnParseSpec { entrypoint: self.spec.entrypoint.clone(), project_dir: self.fixture_dir.clone(), + runtime: self.build_process.runtime_config(&self.fixture_dir), }) .await?; - for expected_source in &expected_sources { - match result_sources + for expected_source in &expected_manifest.sources { + match result_manifest + .sources .iter() .find(|source| source.path == expected_source.path) { None => panic!( "parsing fixture {} did not contain source file {}", - self.fixture_dir - .strip_prefix(env::current_dir()?.join("fixtures"))? - .to_string_lossy(), + self.fixture_label(), expected_source.path.to_string_lossy(), ), Some(source) => { @@ -193,10 +218,21 @@ impl TestFixture { } } } - assert_eq!(expected_sources.len(), result_sources.len()); + assert_eq!( + expected_manifest.sources.len(), + result_manifest.sources.len() + ); Ok(()) } + fn fixture_label(&self) -> String { + self.fixture_dir + .strip_prefix(env::current_dir().unwrap().join("fixtures")) + .unwrap() + .to_string_lossy() + .to_string() + } + fn read_expected_build_results(&self) -> HashMap> { let mut result = HashMap::new(); for build_mode in &[BuildMode::Debug, BuildMode::Release] { @@ -208,9 +244,16 @@ impl TestFixture { result } - fn read_expected_parse_result(&self) -> FnSources { + fn read_expected_parse_result(&self) -> FnManifest { let path = self.fixture_dir.join(".fixture").join("parse.json"); debug_assert!(path.is_file()); - serde_json::from_str(fs::read_to_string(path).unwrap().as_str()).unwrap() + serde_json::from_str(fs::read_to_string(&path).unwrap().as_str()) + .map_err(|err| { + anyhow!( + "failed parsing fixture {} parse.json: {err}", + self.fixture_label() + ) + }) + .unwrap() } }