use std::path::Path;
use oxc_allocator::Allocator;
#[allow(clippy::wildcard_imports, reason = "many AST types used")]
use oxc_ast::ast::*;
use oxc_ast_visit::{Visit, walk};
use oxc_parser::Parser;
use oxc_span::{GetSpan, SourceType, Span};
const ALCHEMY_PACKAGE: &str = "alchemy";
const STACK_FILE_STEM: &str = "alchemy.run";
const PLATFORM_CLASS_SOURCES: &[&str] = &["cloudflare:workers", "@cloudflare/containers"];
#[derive(Debug, Default, PartialEq, Eq)]
pub struct AlchemyMains {
pub targets: Vec<(String, Span)>,
pub own_exports: Vec<String>,
}
impl AlchemyMains {
#[must_use]
pub fn is_empty(&self) -> bool {
self.targets.is_empty() && self.own_exports.is_empty()
}
}
#[must_use]
pub fn extract_alchemy_mains(path: &Path, source: &str) -> AlchemyMains {
if !source.contains(ALCHEMY_PACKAGE) {
return AlchemyMains::default();
}
let Ok(source_type) = SourceType::from_path(path) else {
return AlchemyMains::default();
};
let allocator = Allocator::default();
let program = Parser::new(&allocator, source, source_type).parse().program;
if !imports_alchemy(&program) {
return AlchemyMains::default();
}
let mut collector = MainCollector {
is_stack_file: is_stack_file(path),
names_itself: false,
targets: Vec::new(),
};
collector.visit_program(&program);
let mut targets = collector.targets;
targets.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.start.cmp(&b.1.start)));
targets.dedup_by(|a, b| a.0 == b.0);
let own_exports = if collector.names_itself {
platform_exports(&program)
} else {
Vec::new()
};
AlchemyMains {
targets,
own_exports,
}
}
fn platform_exports(program: &Program<'_>) -> Vec<String> {
let platform_bindings = platform_bindings(program);
let mut names = Vec::new();
for statement in &program.body {
match statement {
Statement::ExportDefaultDeclaration(_) => names.push("default".to_owned()),
Statement::ExportDeclaration(export) => {
if let Declaration::ClassDeclaration(class) = &export.declaration
&& let Some(id) = &class.id
&& class
.heritage
.as_ref()
.and_then(|heritage| root_identifier(&heritage.expression))
.is_some_and(|root| platform_bindings.contains(&root))
{
names.push(id.name.to_string());
}
}
_ => {}
}
}
names
}
fn imports_alchemy(program: &Program<'_>) -> bool {
import_declarations(program).any(|import| is_alchemy_source(import.source.value.as_str()))
}
fn import_declarations<'b, 'a>(
program: &'b Program<'a>,
) -> impl Iterator<Item = &'b ImportDeclaration<'a>> {
program.body.iter().filter_map(|statement| match statement {
Statement::ImportDeclaration(import) => Some(&**import),
_ => None,
})
}
fn is_alchemy_source(source: &str) -> bool {
source == ALCHEMY_PACKAGE
|| source
.strip_prefix(ALCHEMY_PACKAGE)
.is_some_and(|rest| rest.starts_with('/'))
}
fn platform_bindings<'a>(program: &Program<'a>) -> Vec<&'a str> {
import_declarations(program)
.filter(|import| {
let source = import.source.value.as_str();
is_alchemy_source(source) || PLATFORM_CLASS_SOURCES.contains(&source)
})
.flat_map(|import| import.specifiers.iter().flatten())
.map(|specifier| specifier.local().name.as_str())
.collect()
}
fn root_identifier<'a>(expression: &Expression<'a>) -> Option<&'a str> {
let mut current = expression.without_parentheses();
loop {
current = match current {
Expression::Identifier(identifier) => return Some(identifier.name.as_str()),
Expression::StaticMemberExpression(member) => &member.object,
Expression::CallExpression(call) => &call.callee,
Expression::TSInstantiationExpression(instantiation) => &instantiation.expression,
_ => return None,
}
.without_parentheses();
}
}
fn is_stack_file(path: &Path) -> bool {
path.file_stem()
.and_then(|stem| stem.to_str())
.is_some_and(|stem| stem == STACK_FILE_STEM)
}
struct MainCollector {
is_stack_file: bool,
names_itself: bool,
targets: Vec<(String, Span)>,
}
impl<'a> Visit<'a> for MainCollector {
fn visit_object_expression(&mut self, object: &ObjectExpression<'a>) {
if let Some(main) = property_value(object, "main") {
if is_own_file(main) {
self.names_itself = true;
} else if let Some(path) = self.main_path(object, main) {
let end = main.span().end;
self.targets.push((path, Span::new(end, end)));
}
}
walk::walk_object_expression(self, object);
}
}
impl MainCollector {
fn main_path(&self, object: &ObjectExpression<'_>, main: &Expression<'_>) -> Option<String> {
if let Some(path) = file_relative_path(main) {
return Some(path);
}
let raw = relative_target(string_value(main)?)?;
match property_value(object, "rootDir") {
Some(root_dir) => file_relative_dir(root_dir).map(|base| join(&base, raw)),
None => self.is_stack_file.then(|| join(".", raw)),
}
}
}
fn property_value<'b, 'a>(
object: &'b ObjectExpression<'a>,
key: &str,
) -> Option<&'b Expression<'a>> {
object.properties.iter().find_map(|property| {
let ObjectPropertyKind::ObjectProperty(property) = property else {
return None;
};
if property.computed || property.kind != PropertyKind::Init {
return None;
}
(property.key.static_name().as_deref() == Some(key)).then_some(&property.value)
})
}
fn string_value<'b>(expression: &'b Expression<'_>) -> Option<&'b str> {
match expression.without_parentheses() {
Expression::StringLiteral(literal) => Some(literal.value.as_str()),
Expression::TemplateLiteral(template) if template.expressions.is_empty() => template
.quasis
.first()
.and_then(|quasi| quasi.value.cooked.as_ref())
.map(|cooked| cooked.as_str()),
_ => None,
}
}
fn file_relative_path(expression: &Expression<'_>) -> Option<String> {
match expression.without_parentheses() {
Expression::StaticMemberExpression(member)
if matches!(member.property.name.as_str(), "pathname" | "href") =>
{
new_url_path(&member.object)
}
Expression::NewExpression(_) => new_url_path(expression),
Expression::TemplateLiteral(template) => template_dir_path(template),
Expression::CallExpression(call) => {
path_join_call(call).or_else(|| file_url_to_path_call(call))
}
_ => None,
}
}
fn new_url_path(expression: &Expression<'_>) -> Option<String> {
let Expression::NewExpression(new) = expression.without_parentheses() else {
return None;
};
let Expression::Identifier(callee) = &new.callee else {
return None;
};
let [target, base] = new.arguments.as_slice() else {
return None;
};
if callee.name != "URL" || !is_file_url(base.as_expression()?) {
return None;
}
relative_target(string_value(target.as_expression()?)?).map(|target| join(".", target))
}
fn file_url_to_path_call(call: &CallExpression<'_>) -> Option<String> {
let Expression::Identifier(callee) = &call.callee else {
return None;
};
let [argument] = call.arguments.as_slice() else {
return None;
};
(callee.name == "fileURLToPath")
.then(|| new_url_path(argument.as_expression()?))
.flatten()
}
fn template_dir_path(template: &TemplateLiteral<'_>) -> Option<String> {
let [expression] = template.expressions.as_slice() else {
return None;
};
let [head, tail] = template.quasis.as_slice() else {
return None;
};
if !is_dir_name(expression) || !head.value.raw.is_empty() {
return None;
}
let target = tail.value.cooked.as_ref()?.as_str().strip_prefix('/')?;
relative_target(target).map(|target| join(".", target))
}
fn path_join_call(call: &CallExpression<'_>) -> Option<String> {
let Expression::StaticMemberExpression(callee) = &call.callee else {
return None;
};
let Expression::Identifier(object) = &callee.object else {
return None;
};
if object.name != "path" || !matches!(callee.property.name.as_str(), "join" | "resolve") {
return None;
}
let (first, rest) = call.arguments.split_first()?;
if !is_dir_name(first.as_expression()?) || rest.is_empty() {
return None;
}
let mut path = String::from(".");
for argument in rest {
let segment = relative_target(string_value(argument.as_expression()?)?)?;
path = join(&path, segment);
}
Some(path)
}
fn is_own_file(expression: &Expression<'_>) -> bool {
matches!(
import_meta_property(expression),
Some("url" | "filename" | "path")
)
}
fn is_file_url(expression: &Expression<'_>) -> bool {
import_meta_property(expression) == Some("url")
}
fn is_dir_name(expression: &Expression<'_>) -> bool {
match expression.without_parentheses() {
Expression::Identifier(identifier) => identifier.name == "__dirname",
expression => import_meta_property(expression) == Some("dirname"),
}
}
fn import_meta_property<'b>(expression: &'b Expression<'_>) -> Option<&'b str> {
let Expression::StaticMemberExpression(member) = expression.without_parentheses() else {
return None;
};
matches!(member.object, Expression::ImportMeta(_)).then_some(member.property.name.as_str())
}
fn relative_target(target: &str) -> Option<&str> {
if target.is_empty() || target.starts_with('/') || target.contains(':') {
return None;
}
Some(target)
}
fn join(base: &str, segment: &str) -> String {
let mut parts: Vec<&str> = Vec::new();
for part in base.split('/').chain(segment.split('/')) {
match part {
"" | "." => {}
".." if parts.last().is_some_and(|last| *last != "..") => {
parts.pop();
}
part => parts.push(part),
}
}
let joined = parts.join("/");
if parts.first() == Some(&"..") {
joined
} else {
format!("./{joined}")
}
}
fn file_relative_dir(expression: &Expression<'_>) -> Option<String> {
if is_dir_name(expression) {
return Some(".".to_owned());
}
file_relative_path(expression)
}
#[cfg(test)]
mod tests {
use super::*;
fn mains(path: &str, source: &str) -> Vec<String> {
extract_alchemy_mains(Path::new(path), source)
.targets
.into_iter()
.map(|(path, _)| path)
.collect()
}
#[test]
fn new_url_main_forms_are_read() {
let source = r#"
import * as Cloudflare from "alchemy/Cloudflare";
export const a = Cloudflare.Worker("a", { main: new URL("./src/a.ts", import.meta.url).pathname });
export const b = Cloudflare.Worker("b", { main: new URL("./b.ts", import.meta.url).href });
export const c = Cloudflare.Worker("c", { main: new URL("../c.ts", import.meta.url) });
export const d = Cloudflare.Worker("d", { main: fileURLToPath(new URL("./d.ts", import.meta.url)) });
"#;
assert_eq!(
mains("apps/web/infra.ts", source),
vec!["../c.ts", "./b.ts", "./d.ts", "./src/a.ts"]
);
}
#[test]
fn dirname_main_forms_are_read() {
let source = r#"
import path from "node:path";
import * as Cloudflare from "alchemy/Cloudflare";
const a = { main: `${import.meta.dirname}/src/a.ts` };
const b = { main: path.join(import.meta.dirname, "src", "b.ts") };
const c = { main: path.resolve(__dirname, "../c.ts") };
"#;
assert_eq!(
mains("apps/web/infra.ts", source),
vec!["../c.ts", "./src/a.ts", "./src/b.ts"]
);
}
#[test]
fn string_main_resolves_against_a_file_relative_root_dir() {
let source = r#"
import * as Cloudflare from "alchemy/Cloudflare";
const rootDir = new URL("..", import.meta.url).pathname;
export const web = { rootDir: new URL("..", import.meta.url).pathname, main: "src/entry.ts" };
export const unknown = { rootDir, main: "src/other.ts" };
"#;
assert_eq!(
mains("apps/web/src/worker.ts", source),
vec!["../src/entry.ts"]
);
}
#[test]
fn string_main_resolves_against_a_dirname_root_dir() {
let source = r#"
import * as Cloudflare from "alchemy/Cloudflare";
export const a = { rootDir: import.meta.dirname, main: "src/a.ts" };
export const b = { rootDir: __dirname, main: "..b/b.ts" };
"#;
assert_eq!(
mains("apps/web/infra.ts", source),
vec!["./..b/b.ts", "./src/a.ts"]
);
}
#[test]
fn string_main_is_read_only_in_a_stack_file() {
let source = r#"
import * as Cloudflare from "alchemy/Cloudflare";
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts" });
"#;
assert_eq!(mains("app/alchemy.run.ts", source), vec!["./src/worker.ts"]);
assert!(mains("app/src/infra.ts", source).is_empty());
}
#[test]
fn main_that_names_the_declaring_file_credits_default_and_classes() {
let source = r#"
import { DurableObject } from "cloudflare:workers";
import * as Schema from "effect/Schema";
import * as Cloudflare from "alchemy/Cloudflare";
export const Cache = Cloudflare.KV.Namespace("cache");
export class Counter extends Cloudflare.DurableObject<Counter>()("Counter", {}) {}
export class Room extends DurableObject {}
export class Failed extends Schema.TaggedError<Failed>()("Failed", {}) {}
export class Plain {}
export default class Api extends Cloudflare.Worker<Api>()("Api", { main: import.meta.url }) {}
"#;
let result = extract_alchemy_mains(Path::new("src/Api.ts"), source);
assert!(result.targets.is_empty());
assert_eq!(result.own_exports, vec!["Counter", "Room", "default"]);
}
#[test]
fn file_without_an_alchemy_import_is_not_read() {
let source = r#"
import { something } from "alchemy-like";
// alchemy
export const config = { main: new URL("./src/a.ts", import.meta.url).pathname };
export default { main: import.meta.url };
"#;
assert!(extract_alchemy_mains(Path::new("src/config.ts"), source).is_empty());
}
#[test]
fn dynamic_and_absolute_mains_are_not_read() {
let source = r#"
import * as AWS from "alchemy/AWS";
const a = { main: entry };
const b = { main: `${dir}/src/b.ts` };
const c = { main: new URL(name, import.meta.url).pathname };
const d = { main: "/abs/d.ts", rootDir: new URL(".", import.meta.url).pathname };
const e = { [key]: new URL("./e.ts", import.meta.url).pathname };
"#;
assert!(extract_alchemy_mains(Path::new("alchemy.run.ts"), source).is_empty());
}
}