Skip to content

Commit 8a9b71f

Browse files
crowlKatsclaude
andauthored
feat: framework detection for deno compile (#33164)
## Summary - Adds framework detection to `deno compile` so that `deno compile .` (or `deno compile ./myapp`) automatically detects web frameworks and generates the appropriate entrypoint - Supported frameworks: Next.js, Astro, Fresh (1.x/2.x), Remix, SvelteKit, Nuxt, SolidStart, TanStack Start, Vite SSR - Runs `deno task build` before compilation to ensure fresh build output - Uses `import.meta.dirname` in entrypoints so paths resolve against the VFS in compiled binaries Based on framework detection work by @littledivy in the desktop branch. ## Test plan - [x] 39 unit tests for framework detection logic (detection, priority, version parsing, helpers) - [x] 4 spec tests: Vite SSR (with build step), Fresh 1.x (no build), error case (no framework), explicit directory path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Divy Srivastava <dj.srivastava23@gmail.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 56b88b4 commit 8a9b71f

24 files changed

Lines changed: 1146 additions & 1 deletion

File tree

cli/tools/compile.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,125 @@ use super::installer::BinNameResolver;
2727
use crate::args::CliOptions;
2828
use crate::args::CompileFlags;
2929
use crate::args::ConfigFlag;
30+
use crate::args::DenoSubcommand;
3031
use crate::args::Flags;
32+
use crate::args::TypeCheckMode;
3133
use crate::factory::CliFactory;
3234
use crate::standalone::binary::WriteBinOptions;
3335
use crate::standalone::binary::is_standalone_binary;
3436
use crate::util::temp::create_temp_node_modules_dir;
3537

3638
pub async fn compile(
3739
mut flags: Flags,
38-
compile_flags: CompileFlags,
40+
mut compile_flags: CompileFlags,
3941
) -> Result<(), AnyError> {
42+
// Framework detection: when the source is a directory, detect the
43+
// framework and generate an entrypoint automatically.
44+
let source_dir = if compile_flags.source_file == "." {
45+
Some(flags.initial_cwd.clone().unwrap_or_else(|| {
46+
crate::util::env::resolve_cwd(None).unwrap().to_path_buf()
47+
}))
48+
} else {
49+
let path = PathBuf::from(&compile_flags.source_file);
50+
let path = if path.is_absolute() {
51+
path
52+
} else {
53+
flags
54+
.initial_cwd
55+
.clone()
56+
.unwrap_or_else(|| {
57+
crate::util::env::resolve_cwd(None).unwrap().to_path_buf()
58+
})
59+
.join(path)
60+
};
61+
path.is_dir().then_some(path)
62+
};
63+
64+
let _framework_entrypoint_file = if let Some(dir) = source_dir {
65+
if let Some(detection) = super::framework::detect_framework(&dir)? {
66+
log::info!("Detected {} framework", detection.name);
67+
// Run the framework's build step if needed.
68+
if let Some(build_cmd) = &detection.build_command {
69+
log::info!(
70+
"{} {} project...",
71+
colors::green("Building"),
72+
detection.name,
73+
);
74+
let status = std::process::Command::new(&build_cmd[0])
75+
.args(&build_cmd[1..])
76+
.current_dir(&dir)
77+
.status()
78+
.with_context(|| {
79+
format!("Failed to run build command: {}", build_cmd.join(" "))
80+
})?;
81+
if !status.success() {
82+
bail!(
83+
"{} build failed (exit code: {})",
84+
detection.name,
85+
status.code().unwrap_or(-1)
86+
);
87+
}
88+
}
89+
// Enable CJS detection for Node-based frameworks.
90+
flags.unstable_config.detect_cjs = true;
91+
if detection.name == "Next.js"
92+
&& !matches!(flags.type_check_mode, TypeCheckMode::None)
93+
{
94+
log::info!(
95+
"Disabling Deno type checking for Next.js compile; Next handles app compilation itself"
96+
);
97+
flags.type_check_mode = TypeCheckMode::None;
98+
}
99+
// Write a temporary entrypoint file with a random suffix so we
100+
// never overwrite an existing project file.
101+
let entrypoint_path = dir.join(format!(
102+
".deno_compile_entry_{:08x}.ts",
103+
rand::thread_rng().r#gen::<u32>()
104+
));
105+
std::fs::write(&entrypoint_path, detection.entrypoint_code)?;
106+
compile_flags.source_file = entrypoint_path.display().to_string();
107+
if compile_flags.output.is_none()
108+
&& let Some(dir_name) = dir.file_name()
109+
{
110+
compile_flags.output = Some(dir_name.to_string_lossy().into_owned());
111+
}
112+
// Add framework build output to includes, resolved relative to the
113+
// detected app directory so `deno compile ./myapp` picks up
114+
// `./myapp/.next` rather than `./.next`.
115+
for inc in detection.include_paths {
116+
let resolved = dir.join(&inc).display().to_string();
117+
if !compile_flags.include.contains(&resolved) {
118+
compile_flags.include.push(resolved);
119+
}
120+
}
121+
Some(entrypoint_path)
122+
} else {
123+
bail!(
124+
"Could not detect a supported framework in '{}'.\n\
125+
Supported frameworks: Next.js, Astro, Fresh, Remix, SvelteKit, Nuxt, SolidStart, TanStack Start, Vite SSR\n\
126+
Provide an explicit entrypoint instead of a directory.",
127+
dir.display()
128+
);
129+
}
130+
} else {
131+
None
132+
};
133+
134+
// Keep flags.subcommand in sync so resolve_main_module sees the
135+
// rewritten source_file instead of the original directory path.
136+
flags.subcommand = DenoSubcommand::Compile(compile_flags.clone());
137+
138+
// Clean up temp entrypoint on exit.
139+
struct CleanupGuard(Option<PathBuf>);
140+
impl Drop for CleanupGuard {
141+
fn drop(&mut self) {
142+
if let Some(ref path) = self.0 {
143+
let _ = std::fs::remove_file(path);
144+
}
145+
}
146+
}
147+
let _cleanup = CleanupGuard(_framework_entrypoint_file);
148+
40149
// use a temporary directory with a node_modules folder when the user
41150
// specifies an npm package for better compatibility
42151
let _temp_dir =

0 commit comments

Comments
 (0)