use fallow_config::{ScopedUsedClassMemberRule, UsedClassMemberRule};
use super::Plugin;
const ENABLERS: &[&str] = &[
"ember-source",
"ember-cli",
"@embroider/core",
"@embroider/compat",
"@glimmer/component",
];
const TOOLING_DEPENDENCIES: &[&str] = &[
"ember-source",
"ember-cli",
"ember-cli-htmlbars",
"ember-cli-babel",
"ember-auto-import",
"@embroider/core",
"@glint/core",
"@glint/environment-ember-loose",
"@glint/environment-ember-template-imports",
"ember-cli-test-loader",
"ember-exam",
"ember-template-lint",
"ember-template-imports",
"ember-source-channel-url",
"@ember/optional-features",
"ember-cli-dependency-checker",
"ember-cli-inject-live-reload",
"ember-cli-sri",
"ember-cli-terser",
"loader.js",
"broccoli-asset-rev",
"ember-cli-app-version",
"ember-export-application-global",
"@tsconfig/ember",
"@glint/tsserver-plugin",
];
const COMPONENT_MEMBERS: &[&str] = &[
"willDestroy",
"didInsertElement",
"didRender",
"didUpdate",
"didReceiveAttrs",
"willRender",
"willUpdate",
"willClearRender",
"willDestroyElement",
"didDestroyElement",
];
const ROUTE_MEMBERS: &[&str] = &[
"model",
"beforeModel",
"afterModel",
"setupController",
"resetController",
"redirect",
"serialize",
"deserialize",
"activate",
"deactivate",
"buildRouteInfoMetadata",
"actions",
"queryParams",
"templateName",
"controllerName",
"init",
"willDestroy",
"destroy",
];
const CONTROLLER_MEMBERS: &[&str] = &[
"actions",
"queryParams",
"templateName",
"controllerName",
"init",
"willDestroy",
"destroy",
];
const SERVICE_MEMBERS: &[&str] = &["init", "willDestroy", "destroy"];
const HELPER_MEMBERS: &[&str] = &[
"compute",
"recompute",
"init",
"willDestroy",
"destroy",
];
const MODIFIER_MEMBERS: &[&str] = &[
"modify",
"willDestroy",
"didReceiveArguments",
"didInstall",
"didUpdateArguments",
"willRemove",
];
const APPLICATION_MEMBERS: &[&str] = &[
"ready",
"customEvents",
"eventDispatcher",
"resolver",
"rootElement",
"init",
"willDestroy",
"destroy",
];
const ROUTER_MEMBERS: &[&str] = &[
"map",
"location",
"rootURL",
"willTransition",
"didTransition",
];
const VIRTUAL_MODULE_PREFIXES: &[&str] = &[
"ember/",
"@ember/application",
"@ember/array",
"@ember/canary-features",
"@ember/component",
"@ember/controller",
"@ember/debug",
"@ember/destroyable",
"@ember/engine",
"@ember/enumerable",
"@ember/error",
"@ember/helper",
"@ember/instrumentation",
"@ember/modifier",
"@ember/object",
"@ember/owner",
"@ember/renderer",
"@ember/routing",
"@ember/runloop",
"@ember/service",
"@ember/template",
"@ember/utils",
"@ember/version",
];
const ENTRY_PATTERNS: &[&str] = &[
"app/app.{js,ts,gjs,gts}",
"app/router.{js,ts}",
"app/index.html",
"app/components/**/*.{js,ts,gjs,gts,hbs}",
"app/routes/**/*.{js,ts,gjs,gts}",
"app/controllers/**/*.{js,ts}",
"app/templates/**/*.{hbs,gjs,gts}",
"app/models/**/*.{js,ts}",
"app/services/**/*.{js,ts}",
"app/helpers/**/*.{js,ts,gjs,gts}",
"app/modifiers/**/*.{js,ts}",
"app/initializers/**/*.{js,ts}",
"app/instance-initializers/**/*.{js,ts}",
"app/adapters/**/*.{js,ts}",
"app/serializers/**/*.{js,ts}",
"app/transforms/**/*.{js,ts}",
"tests/test-helper.{js,ts}",
"tests/index.html",
"tests/**/*-test.{js,ts,gjs,gts}",
"config/environment.js",
"config/targets.js",
"config/optional-features.json",
"config/deprecation-workflow.js",
"ember-cli-build.js",
"testem.js",
];
fn scoped_rule(extends: &str, members: &[&str]) -> UsedClassMemberRule {
UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
extends: Some(extends.to_string()),
implements: None,
members: members.iter().map(|s| (*s).to_string()).collect(),
})
}
pub struct EmberPlugin;
impl Plugin for EmberPlugin {
fn name(&self) -> &'static str {
"ember"
}
fn enablers(&self) -> &'static [&'static str] {
ENABLERS
}
fn tooling_dependencies(&self) -> &'static [&'static str] {
TOOLING_DEPENDENCIES
}
fn used_class_member_rules(&self) -> Vec<UsedClassMemberRule> {
vec![
scoped_rule("Component", COMPONENT_MEMBERS),
scoped_rule("Route", ROUTE_MEMBERS),
scoped_rule("Controller", CONTROLLER_MEMBERS),
scoped_rule("Service", SERVICE_MEMBERS),
scoped_rule("Helper", HELPER_MEMBERS),
scoped_rule("Modifier", MODIFIER_MEMBERS),
scoped_rule("Application", APPLICATION_MEMBERS),
scoped_rule("Router", ROUTER_MEMBERS),
]
}
fn virtual_module_prefixes(&self) -> &'static [&'static str] {
VIRTUAL_MODULE_PREFIXES
}
fn entry_patterns(&self) -> &'static [&'static str] {
ENTRY_PATTERNS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enablers_cover_classic_embroider_and_glimmer() {
let plugin = EmberPlugin;
assert!(plugin.enablers().contains(&"ember-source"));
assert!(plugin.enablers().contains(&"@embroider/core"));
assert!(plugin.enablers().contains(&"@glimmer/component"));
}
#[test]
fn tooling_dependencies_cover_runtime_only_packages() {
let plugin = EmberPlugin;
let deps = plugin.tooling_dependencies();
assert!(deps.contains(&"ember-source"));
assert!(deps.contains(&"ember-cli-htmlbars"));
assert!(deps.contains(&"@embroider/core"));
assert!(deps.contains(&"@glint/core"));
assert!(deps.contains(&"ember-exam"));
assert!(deps.contains(&"loader.js"));
assert!(deps.contains(&"broccoli-asset-rev"));
assert!(deps.contains(&"ember-cli-app-version"));
assert!(deps.contains(&"ember-export-application-global"));
assert!(deps.contains(&"@tsconfig/ember"));
assert!(deps.contains(&"@glint/tsserver-plugin"));
}
#[test]
fn tooling_dependencies_omits_source_imported_packages() {
let plugin = EmberPlugin;
let deps = plugin.tooling_dependencies();
for name in [
"@glimmer/component",
"@glimmer/tracking",
"@glimmer/env",
"@glint/template",
"@ember/test-helpers",
"ember-qunit",
"qunit",
"qunit-dom",
"ember-data",
"@ember-data/store",
"@ember-data/model",
"@embroider/macros",
"@embroider/router",
"@embroider/test-setup",
"@embroider/webpack",
"@embroider/vite",
"@embroider/addon-shim",
"ember-load-initializers",
"ember-resolver",
] {
assert!(
!deps.contains(&name),
"{name} is imported from source in modern Ember; remove from tooling_dependencies"
);
}
}
#[test]
fn lifecycle_rules_scope_component_members_to_glimmer_component() {
let rules = EmberPlugin.used_class_member_rules();
let component_rule = rules.iter().find_map(|r| match r {
UsedClassMemberRule::Scoped(s) if s.extends.as_deref() == Some("Component") => Some(s),
_ => None,
});
let component_rule = component_rule.expect("Component-scoped rule missing");
assert!(component_rule.members.iter().any(|m| m == "willDestroy"));
assert!(
component_rule
.members
.iter()
.any(|m| m == "didInsertElement")
);
}
#[test]
fn lifecycle_rules_scope_route_members_to_route_class() {
let rules = EmberPlugin.used_class_member_rules();
let route_rule = rules.iter().find_map(|r| match r {
UsedClassMemberRule::Scoped(s) if s.extends.as_deref() == Some("Route") => Some(s),
_ => None,
});
let route_rule = route_rule.expect("Route-scoped rule missing");
assert!(route_rule.members.iter().any(|m| m == "model"));
assert!(route_rule.members.iter().any(|m| m == "beforeModel"));
assert!(route_rule.members.iter().any(|m| m == "setupController"));
}
#[test]
fn unrelated_classes_get_no_lifecycle_rule_match() {
let rules = EmberPlugin.used_class_member_rules();
for r in &rules {
let UsedClassMemberRule::Scoped(s) = r else {
continue;
};
assert!(!s.matches_heritage(Some("UserService"), &[]));
}
}
#[test]
fn entry_patterns_cover_classic_layout() {
let plugin = EmberPlugin;
let patterns = plugin.entry_patterns();
assert!(patterns.contains(&"app/components/**/*.{js,ts,gjs,gts,hbs}"));
assert!(patterns.contains(&"tests/**/*-test.{js,ts,gjs,gts}"));
}
#[test]
fn entry_patterns_do_not_include_v1_addon_layout() {
let plugin = EmberPlugin;
let patterns = plugin.entry_patterns();
for v1_only in [
"addon/**/*.{js,ts,gjs,gts,hbs}",
"addon-test-support/**/*.{js,ts,gjs,gts}",
] {
assert!(
!patterns.contains(&v1_only),
"{v1_only} must not be in entry_patterns (v1 addons are out \
of scope); current patterns = {patterns:?}"
);
}
}
fn is_covered(prefixes: &[&str], spec: &str) -> bool {
prefixes
.iter()
.any(|prefix| crate::analyze::matches_virtual_prefix(prefix, spec))
}
#[test]
fn virtual_module_prefixes_cover_ember_source_runtime() {
let prefixes = EmberPlugin.virtual_module_prefixes();
for spec in [
"@ember/object",
"@ember/object/computed",
"@ember/template",
"@ember/service",
"@ember/runloop",
"@ember/utils",
"@ember/routing/router-service",
"@ember/helper",
"@ember/modifier",
"@ember/application",
"@ember/component",
"@ember/component/helper",
"@ember/controller",
"@ember/debug",
"@ember/destroyable",
"@ember/object/proxy",
"@ember/routing/route",
"@ember/template-compilation",
"@ember/template-factory",
"@ember/owner",
"ember",
] {
assert!(
is_covered(prefixes, spec),
"expected `{spec}` to be silenced by the virtual-module \
prefix list (it is rewritten by Embroider / the AMD loader \
and not resolvable through node_modules); prefixes = \
{prefixes:?}",
);
}
}
#[test]
fn virtual_module_prefixes_do_not_swallow_real_ember_npm_packages() {
let prefixes = EmberPlugin.virtual_module_prefixes();
for real in [
"@ember/test-helpers",
"@ember/render-modifiers",
"@ember/test-waiters",
"@ember/string",
"@ember/jquery",
"@ember/legacy-built-in-components",
"@ember/optional-features",
] {
assert!(
!is_covered(prefixes, real),
"`{real}` is a real npm package and must NOT be covered by \
the virtual-module prefix list; prefixes = {prefixes:?}",
);
}
}
#[test]
fn virtual_module_prefixes_do_not_swallow_ember_dash_packages() {
let prefixes = EmberPlugin.virtual_module_prefixes();
for spec in [
"@ember-data/store",
"@ember-data/model",
"@glimmer/component",
"@glimmer/tracking",
"ember-source",
"ember-cli",
"ember-data",
"ember-in-viewport",
"ember-template-lint",
] {
assert!(
!is_covered(prefixes, spec),
"`{spec}` must NOT be covered by the virtual-module prefix \
list; prefixes = {prefixes:?}",
);
}
}
}