Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 178 additions & 1 deletion rs_lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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<PathBuf> = files
.include
.as_ref()
.map(|s| s.inner().iter().filter_map(|p| p.base_path()).collect())
.unwrap_or_default();

let mut to_restore: Vec<PathOrPattern> = 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<PathOrPattern> = 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,
Expand Down Expand Up @@ -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,
);
}
}
Loading