diff --git a/changelog.d/plugins-dir-optional.fixed.md b/changelog.d/plugins-dir-optional.fixed.md new file mode 100644 index 0000000000..796041ec1e --- /dev/null +++ b/changelog.d/plugins-dir-optional.fixed.md @@ -0,0 +1 @@ +- The legacy `plugins/` directory is now optional. The scaffold's `Application.cfc` jar-scan and the framework plugin loader (`Plugins.cfc` `$folders()`/`$files()`) now guard their directory listing with `DirectoryExists`, so an app that has removed `plugins/` (the common case now that packages live in `vendor//`) no longer errors at startup on engines whose directory listing throws on a missing path — Lucee/Adobe tolerate a missing dir, but stricter engines (e.g. RustCFML) did not. The plugins-directory lookup is deprecated and slated for removal in the next major diff --git a/cli/lucli/templates/app/public/Application.cfc b/cli/lucli/templates/app/public/Application.cfc index d7d0bfc1f9..0a38c6aace 100644 --- a/cli/lucli/templates/app/public/Application.cfc +++ b/cli/lucli/templates/app/public/Application.cfc @@ -24,13 +24,14 @@ component output="false" { this.sessionManagement = true; // If a plugin has a jar or class file, automatically add the mapping to this.javasettings. + // Legacy plugins system (DEPRECATED — superseded by vendor// packages). + // Only scan when a plugins/ directory exists, so a removed plugins/ dir does not + // error on engines whose directoryList() throws on a missing path (e.g. RustCFML; + // Lucee tolerates it). This lookup is slated for removal in the next major. this.wheels.pluginDir = this.appDir & "../plugins"; - this.wheels.pluginFolders = DirectoryList( - this.wheels.pluginDir, - "true", - "path", - "*.class|*.jar|*.java" - ); + this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir) + ? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java") + : []; for (this.wheels.folder in this.wheels.pluginFolders) { if (!StructKeyExists(this, "javaSettings")) { diff --git a/examples/starter-app/public/Application.cfc b/examples/starter-app/public/Application.cfc index 5c7a53d5e4..954ba4e7dc 100644 --- a/examples/starter-app/public/Application.cfc +++ b/examples/starter-app/public/Application.cfc @@ -26,13 +26,14 @@ component output="false" { this.sessionManagement = true; // If a plugin has a jar or class file, automatically add the mapping to this.javasettings. + // Legacy plugins system (DEPRECATED — superseded by vendor// packages). + // Only scan when a plugins/ directory exists, so a removed plugins/ dir does not + // error on engines whose directoryList() throws on a missing path (e.g. RustCFML; + // Lucee tolerates it). This lookup is slated for removal in the next major. this.wheels.pluginDir = this.appDir & "../plugins"; - this.wheels.pluginFolders = DirectoryList( - this.wheels.pluginDir, - "true", - "path", - "*.class|*.jar|*.java" - ); + this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir) + ? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java") + : []; for (this.wheels.folder in this.wheels.pluginFolders) { if (!StructKeyExists(this, "javaSettings")) { diff --git a/examples/tweet/public/Application.cfc b/examples/tweet/public/Application.cfc index 5c7a53d5e4..954ba4e7dc 100755 --- a/examples/tweet/public/Application.cfc +++ b/examples/tweet/public/Application.cfc @@ -26,13 +26,14 @@ component output="false" { this.sessionManagement = true; // If a plugin has a jar or class file, automatically add the mapping to this.javasettings. + // Legacy plugins system (DEPRECATED — superseded by vendor// packages). + // Only scan when a plugins/ directory exists, so a removed plugins/ dir does not + // error on engines whose directoryList() throws on a missing path (e.g. RustCFML; + // Lucee tolerates it). This lookup is slated for removal in the next major. this.wheels.pluginDir = this.appDir & "../plugins"; - this.wheels.pluginFolders = DirectoryList( - this.wheels.pluginDir, - "true", - "path", - "*.class|*.jar|*.java" - ); + this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir) + ? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java") + : []; for (this.wheels.folder in this.wheels.pluginFolders) { if (!StructKeyExists(this, "javaSettings")) { diff --git a/public/Application.cfc b/public/Application.cfc index ab004fb993..b1eb617a63 100644 --- a/public/Application.cfc +++ b/public/Application.cfc @@ -37,13 +37,14 @@ component output="false" { this.sessionManagement = true; // If a plugin has a jar or class file, automatically add the mapping to this.javasettings. + // Legacy plugins system (DEPRECATED — superseded by vendor// packages). + // Only scan when a plugins/ directory exists, so a removed plugins/ dir does not + // error on engines whose directoryList() throws on a missing path (e.g. RustCFML; + // Lucee tolerates it). This lookup is slated for removal in the next major. this.wheels.pluginDir = this.appDir & "../plugins"; - this.wheels.pluginFolders = DirectoryList( - this.wheels.pluginDir, - "true", - "path", - "*.class|*.jar|*.java" - ); + this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir) + ? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java") + : []; for (this.wheels.folder in this.wheels.pluginFolders) { if (!StructKeyExists(this, "javaSettings")) { diff --git a/vendor/wheels/Plugins.cfc b/vendor/wheels/Plugins.cfc index 5fcec9b62d..3386df7be8 100644 --- a/vendor/wheels/Plugins.cfc +++ b/vendor/wheels/Plugins.cfc @@ -1041,6 +1041,14 @@ component output="false" extends="wheels.Global"{ } public query function $folders() { + // The legacy plugins/ directory is deprecated (superseded by vendor// + // packages) and may be absent. Skip the scan when it does not exist so + // engines whose directory listing throws on a missing path (e.g. RustCFML) + // don't fail at boot; Lucee/Adobe return empty for a missing dir anyway. + // Slated for removal with the plugins system in the next major. + if (!DirectoryExists(variables.$class.pluginPathFull)) { + return QueryNew("name,directory,type"); + } local.query = $directory( action = "list", directory = variables.$class.pluginPathFull, @@ -1086,6 +1094,10 @@ component output="false" extends="wheels.Global"{ } public query function $files() { + // See $folders(): the deprecated plugins/ directory may be absent. + if (!DirectoryExists(variables.$class.pluginPathFull)) { + return QueryNew("name,directory,type"); + } local.query = $directory( action = "list", directory = variables.$class.pluginPathFull, diff --git a/vendor/wheels/tests/specs/pluginsMissingDirSpec.cfc b/vendor/wheels/tests/specs/pluginsMissingDirSpec.cfc new file mode 100644 index 0000000000..7267433988 --- /dev/null +++ b/vendor/wheels/tests/specs/pluginsMissingDirSpec.cfc @@ -0,0 +1,67 @@ +component extends="wheels.WheelsTest" { + + function run() { + + g = application.wo + + // The legacy plugins/ directory is deprecated (superseded by vendor// + // packages) and apps are expected to remove it. The plugin loader must not + // error when it is absent — Lucee/Adobe return empty for a missing dir, but + // stricter engines (e.g. RustCFML) throw on directory listing of a missing + // path, which previously failed onApplicationStart. $folders()/$files() now + // short-circuit to an empty query when the directory does not exist. + describe("plugin loader with an absent plugins/ directory", () => { + + missingPath = "/wheels/tests/_assets/plugins/__this_directory_does_not_exist__" + + it("initializes without throwing when the plugins directory is missing", () => { + var config = { + path = "wheels", + fileName = "Plugins", + method = "$init", + pluginPath = missingPath, + deletePluginDirectories = false, + overwritePlugins = false, + loadIncompatiblePlugins = true + } + var state = {thrown = false} + try { + pluginObj = $pluginObj(config) + } catch (any e) { + state.thrown = true + } + expect(state.thrown).toBeFalse() + }) + + it("$folders() returns an empty query for a missing directory", () => { + var config = { + path = "wheels", fileName = "Plugins", method = "$init", pluginPath = missingPath, + deletePluginDirectories = false, overwritePlugins = false, loadIncompatiblePlugins = true + } + var pluginObj = $pluginObj(config) + expect(pluginObj.$folders().recordCount).toBe(0) + }) + + it("$files() returns an empty query for a missing directory", () => { + var config = { + path = "wheels", fileName = "Plugins", method = "$init", pluginPath = missingPath, + deletePluginDirectories = false, overwritePlugins = false, loadIncompatiblePlugins = true + } + var pluginObj = $pluginObj(config) + expect(pluginObj.$files().recordCount).toBe(0) + }) + }) + } + + // Mirror the sibling plugin specs (pluginsSpec.cfc:549, pluginsModernSpec, + // pluginsSemverSpec, pluginsManifestIntegrationSpec): a component-level + // helper that instantiates wheels.Plugins via $createObjectFromRoot and + // dispatches $init with the full config — INCLUDING pluginPath. Without it, + // $pluginObj(config) resolves to the parameterless Global.$pluginObj() that + // WheelsTest auto-binds, which ignores config and returns the cached PluginObj + // pointing at the real plugins/ dir — so the missing-path branch (the fix) + // never runs and these specs pass for the wrong reason. + function $pluginObj(required struct config) { + return g.$createObjectFromRoot(argumentCollection = arguments.config) + } +}