diff --git a/rs_lib/src/lib.rs b/rs_lib/src/lib.rs index 99e46e5..2ce8a84 100644 --- a/rs_lib/src/lib.rs +++ b/rs_lib/src/lib.rs @@ -1,5 +1,6 @@ use deno_config::glob::FileCollector; use deno_config::glob::FilePatterns; +use deno_config::glob::PathOrPattern; use deno_config::glob::PathOrPatternSet; use deno_config::workspace::WorkspaceDirectory; use deno_config::workspace::WorkspaceDiscoverOptions; @@ -118,7 +119,16 @@ fn inner_resolve_config( .append(exclude.into_path_or_patterns().into_iter()); } - if let Some(config) = workspace_dir.to_deploy_config(pattern)? { + if let Some(mut config) = workspace_dir.to_deploy_config(pattern)? { + // Workaround for deno_config v0.97.0: `to_deploy_config` calls + // `exclude_includes_with_member_for_base_for_root` which strips any + // user-supplied `deploy.include` pattern whose base path points inside + // a workspace member, even when that member has no competing deploy + // config of its own. That breaks `deno deploy` from a workspace root + // whose root deploy config includes member files (see + // denoland/deploy-cli#90). Restore any user-listed include that was + // stripped. + restore_stripped_member_includes(&workspace_dir, &mut config.files, debug)?; debug_log( debug, &format!( @@ -171,6 +181,80 @@ fn inner_resolve_config( } } +/// Restore `deploy.include` entries that +/// `WorkspaceDirectory::exclude_includes_with_member_for_base_for_root` +/// dropped because their base path falls inside a workspace member. +/// +/// The upstream strip is over-eager: the user explicitly listed those paths, +/// and (in the deploy-from-workspace-root case) the workspace member typically +/// has no competing `deploy` block, so the root config is the only place those +/// files can come from. We re-add the missing entries by reading the raw +/// `deploy.include` from whichever deno.json owns the `deploy` block. +fn restore_stripped_member_includes( + workspace_dir: &WorkspaceDirectory, + files: &mut FilePatterns, + debug: bool, +) -> Result<(), anyhow::Error> { + let raw_config = workspace_dir + .member_deno_json() + .filter(|c| c.json.deploy.is_some()) + .map(|c| c.to_deploy_config()) + .or_else(|| { + workspace_dir + .workspace + .root_deno_json() + .filter(|c| c.json.deploy.is_some()) + .map(|c| c.to_deploy_config()) + }) + .transpose()? + .flatten(); + let Some(raw_config) = raw_config else { + return Ok(()); + }; + let Some(raw_include) = raw_config.files.include else { + return Ok(()); + }; + + let existing_bases: Vec = files + .include + .as_ref() + .map(|s| s.inner().iter().filter_map(|p| p.base_path()).collect()) + .unwrap_or_default(); + + let mut to_restore: Vec = Vec::new(); + for pattern in raw_include.into_path_or_patterns() { + let Some(base) = pattern.base_path() else { + // Patterns without a base_path (e.g. RemoteUrl) are never stripped by + // the upstream function, so they must already be present; skip. + continue; + }; + if existing_bases.iter().any(|b| b == &base) { + continue; + } + debug_log( + debug, + &format!( + "restoring stripped include {:?} (base={:?})", + pattern, base, + ), + ); + to_restore.push(pattern); + } + + if to_restore.is_empty() { + return Ok(()); + } + + let mut combined: Vec = files + .include + .take() + .map(|s| s.into_path_or_patterns()) + .unwrap_or_default(); + combined.extend(to_restore); + files.include = Some(PathOrPatternSet::new(combined)); + Ok(()) +} + fn collect_files( real_sys: &sys_traits::impls::RealSys, root_path: PathBuf, @@ -301,4 +385,97 @@ mod tests { result.files, ); } + + // Regression test for denoland/deploy-cli#90: a workspace-root + // `deploy.include` pointing at a workspace member should include the + // matching member files. Upstream `deno_config` 0.97 strips these + // entries; the `restore_stripped_member_includes` patch re-adds them. + #[test] + fn workspace_root_deploy_include_targeting_member_glob() { + let temp = TempDir::new().unwrap(); + let root = temp.path(); + write_file( + root, + "deno.json", + r#"{ + "workspace": ["./packages/backend"], + "deploy": { + "org": "myorg", + "app": "myapp", + "include": ["./packages/backend/**"] + } + }"#, + ); + write_file(root, "packages/backend/deno.json", "{}"); + write_file(root, "packages/backend/main.ts", "Deno.serve(() => new Response('hi'));"); + write_file(root, "packages/backend/extra.txt", "hello\n"); + + let result = inner_resolve_config( + root.to_string_lossy().into_owned(), + Vec::new(), + false, + false, + ) + .unwrap(); + + let main_ts = root.join("packages/backend/main.ts"); + let extra_txt = root.join("packages/backend/extra.txt"); + assert!( + result.files.iter().any(|f| Path::new(f) == main_ts.as_path()), + "expected {} in deploy files; got {:?}", + main_ts.display(), + result.files, + ); + assert!( + result.files.iter().any(|f| Path::new(f) == extra_txt.as_path()), + "expected {} in deploy files; got {:?}", + extra_txt.display(), + result.files, + ); + } + + // Same regression but with an explicit file include rather than a glob. + #[test] + fn workspace_root_deploy_include_targeting_member_file() { + let temp = TempDir::new().unwrap(); + let root = temp.path(); + write_file( + root, + "deno.json", + r#"{ + "workspace": ["./packages/backend"], + "deploy": { + "org": "myorg", + "app": "myapp", + "include": ["./packages/backend/main.ts"] + } + }"#, + ); + write_file(root, "packages/backend/deno.json", "{}"); + write_file(root, "packages/backend/main.ts", "Deno.serve(() => new Response('hi'));"); + write_file(root, "packages/backend/extra.txt", "should-not-be-included\n"); + + let result = inner_resolve_config( + root.to_string_lossy().into_owned(), + Vec::new(), + false, + false, + ) + .unwrap(); + + let main_ts = root.join("packages/backend/main.ts"); + let extra_txt = root.join("packages/backend/extra.txt"); + assert!( + result.files.iter().any(|f| Path::new(f) == main_ts.as_path()), + "expected {} in deploy files; got {:?}", + main_ts.display(), + result.files, + ); + assert!( + !result.files.iter().any(|f| Path::new(f) == extra_txt.as_path()), + "did not expect {} in deploy files; got {:?}", + extra_txt.display(), + result.files, + ); + } }