Skip to content

Commit b1a7d84

Browse files
bpamiriPeter Amiriclaude
authored
fix(plugin): make the deprecated plugins/ directory optional (#3211)
* fix(plugin): make the deprecated plugins/ directory optional The legacy plugins/ directory is superseded by vendor/<name>/ packages and apps are expected to remove it. But two code paths still listed it unconditionally and threw when it was absent on engines whose directory listing errors on a missing path (e.g. RustCFML; Lucee/Adobe return empty): - The scaffold's public/Application.cfc jar-scan (this.javaSettings.LoadPaths loop) — guarded with DirectoryExists; mirrored into the demo app and the tweet/starter-app examples. - The framework plugin loader Plugins.cfc $folders()/$files() — now short-circuit to an empty query when the plugins directory does not exist. Behavior is unchanged when plugins/ exists (the scan runs as before); when it is absent, no plugins load and no error is raised. Adds pluginsMissingDirSpec (init + $folders()/$files() against a non-existent path). The lookup is deprecated and slated for removal in the next major. Signed-off-by: Peter Amiri <petera@pai.com> * test(plugin): exercise the missing-dir guard via the $pluginObj helper The spec called $pluginObj(config) without defining the helper the four sibling plugin specs use, so it resolved to the parameterless Global.$pluginObj() that WheelsTest auto-binds — which ignores config and returns the cached PluginObj pointing at the real plugins/ dir. The missing-path branch (the $folders()/$files() DirectoryExists guards, the actual fix) was never executed; the assertions passed for the wrong reason. Add the same component-level $pluginObj helper the siblings use so $createObjectFromRoot dispatches $init with pluginPath=missingPath, building a Plugins instance bound to the non-existent path. The three assertions now genuinely exercise the guard. Addresses wheels-bot review on #3211. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com> --------- Signed-off-by: Peter Amiri <petera@pai.com> Co-authored-by: Peter Amiri <petera@pai.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e3f64e4 commit b1a7d84

7 files changed

Lines changed: 108 additions & 24 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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/<name>/`) 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

cli/lucli/templates/app/public/Application.cfc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ component output="false" {
2424
this.sessionManagement = true;
2525

2626
// If a plugin has a jar or class file, automatically add the mapping to this.javasettings.
27+
// Legacy plugins system (DEPRECATED — superseded by vendor/<name>/ packages).
28+
// Only scan when a plugins/ directory exists, so a removed plugins/ dir does not
29+
// error on engines whose directoryList() throws on a missing path (e.g. RustCFML;
30+
// Lucee tolerates it). This lookup is slated for removal in the next major.
2731
this.wheels.pluginDir = this.appDir & "../plugins";
28-
this.wheels.pluginFolders = DirectoryList(
29-
this.wheels.pluginDir,
30-
"true",
31-
"path",
32-
"*.class|*.jar|*.java"
33-
);
32+
this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir)
33+
? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java")
34+
: [];
3435

3536
for (this.wheels.folder in this.wheels.pluginFolders) {
3637
if (!StructKeyExists(this, "javaSettings")) {

examples/starter-app/public/Application.cfc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ component output="false" {
2626
this.sessionManagement = true;
2727

2828
// If a plugin has a jar or class file, automatically add the mapping to this.javasettings.
29+
// Legacy plugins system (DEPRECATED — superseded by vendor/<name>/ packages).
30+
// Only scan when a plugins/ directory exists, so a removed plugins/ dir does not
31+
// error on engines whose directoryList() throws on a missing path (e.g. RustCFML;
32+
// Lucee tolerates it). This lookup is slated for removal in the next major.
2933
this.wheels.pluginDir = this.appDir & "../plugins";
30-
this.wheels.pluginFolders = DirectoryList(
31-
this.wheels.pluginDir,
32-
"true",
33-
"path",
34-
"*.class|*.jar|*.java"
35-
);
34+
this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir)
35+
? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java")
36+
: [];
3637

3738
for (this.wheels.folder in this.wheels.pluginFolders) {
3839
if (!StructKeyExists(this, "javaSettings")) {

examples/tweet/public/Application.cfc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ component output="false" {
2626
this.sessionManagement = true;
2727

2828
// If a plugin has a jar or class file, automatically add the mapping to this.javasettings.
29+
// Legacy plugins system (DEPRECATED — superseded by vendor/<name>/ packages).
30+
// Only scan when a plugins/ directory exists, so a removed plugins/ dir does not
31+
// error on engines whose directoryList() throws on a missing path (e.g. RustCFML;
32+
// Lucee tolerates it). This lookup is slated for removal in the next major.
2933
this.wheels.pluginDir = this.appDir & "../plugins";
30-
this.wheels.pluginFolders = DirectoryList(
31-
this.wheels.pluginDir,
32-
"true",
33-
"path",
34-
"*.class|*.jar|*.java"
35-
);
34+
this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir)
35+
? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java")
36+
: [];
3637

3738
for (this.wheels.folder in this.wheels.pluginFolders) {
3839
if (!StructKeyExists(this, "javaSettings")) {

public/Application.cfc

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ component output="false" {
3737
this.sessionManagement = true;
3838

3939
// If a plugin has a jar or class file, automatically add the mapping to this.javasettings.
40+
// Legacy plugins system (DEPRECATED — superseded by vendor/<name>/ packages).
41+
// Only scan when a plugins/ directory exists, so a removed plugins/ dir does not
42+
// error on engines whose directoryList() throws on a missing path (e.g. RustCFML;
43+
// Lucee tolerates it). This lookup is slated for removal in the next major.
4044
this.wheels.pluginDir = this.appDir & "../plugins";
41-
this.wheels.pluginFolders = DirectoryList(
42-
this.wheels.pluginDir,
43-
"true",
44-
"path",
45-
"*.class|*.jar|*.java"
46-
);
45+
this.wheels.pluginFolders = DirectoryExists(this.wheels.pluginDir)
46+
? DirectoryList(this.wheels.pluginDir, "true", "path", "*.class|*.jar|*.java")
47+
: [];
4748

4849
for (this.wheels.folder in this.wheels.pluginFolders) {
4950
if (!StructKeyExists(this, "javaSettings")) {

vendor/wheels/Plugins.cfc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,14 @@ component output="false" extends="wheels.Global"{
10411041
}
10421042

10431043
public query function $folders() {
1044+
// The legacy plugins/ directory is deprecated (superseded by vendor/<name>/
1045+
// packages) and may be absent. Skip the scan when it does not exist so
1046+
// engines whose directory listing throws on a missing path (e.g. RustCFML)
1047+
// don't fail at boot; Lucee/Adobe return empty for a missing dir anyway.
1048+
// Slated for removal with the plugins system in the next major.
1049+
if (!DirectoryExists(variables.$class.pluginPathFull)) {
1050+
return QueryNew("name,directory,type");
1051+
}
10441052
local.query = $directory(
10451053
action = "list",
10461054
directory = variables.$class.pluginPathFull,
@@ -1086,6 +1094,10 @@ component output="false" extends="wheels.Global"{
10861094
}
10871095

10881096
public query function $files() {
1097+
// See $folders(): the deprecated plugins/ directory may be absent.
1098+
if (!DirectoryExists(variables.$class.pluginPathFull)) {
1099+
return QueryNew("name,directory,type");
1100+
}
10891101
local.query = $directory(
10901102
action = "list",
10911103
directory = variables.$class.pluginPathFull,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
component extends="wheels.WheelsTest" {
2+
3+
function run() {
4+
5+
g = application.wo
6+
7+
// The legacy plugins/ directory is deprecated (superseded by vendor/<name>/
8+
// packages) and apps are expected to remove it. The plugin loader must not
9+
// error when it is absent — Lucee/Adobe return empty for a missing dir, but
10+
// stricter engines (e.g. RustCFML) throw on directory listing of a missing
11+
// path, which previously failed onApplicationStart. $folders()/$files() now
12+
// short-circuit to an empty query when the directory does not exist.
13+
describe("plugin loader with an absent plugins/ directory", () => {
14+
15+
missingPath = "/wheels/tests/_assets/plugins/__this_directory_does_not_exist__"
16+
17+
it("initializes without throwing when the plugins directory is missing", () => {
18+
var config = {
19+
path = "wheels",
20+
fileName = "Plugins",
21+
method = "$init",
22+
pluginPath = missingPath,
23+
deletePluginDirectories = false,
24+
overwritePlugins = false,
25+
loadIncompatiblePlugins = true
26+
}
27+
var state = {thrown = false}
28+
try {
29+
pluginObj = $pluginObj(config)
30+
} catch (any e) {
31+
state.thrown = true
32+
}
33+
expect(state.thrown).toBeFalse()
34+
})
35+
36+
it("$folders() returns an empty query for a missing directory", () => {
37+
var config = {
38+
path = "wheels", fileName = "Plugins", method = "$init", pluginPath = missingPath,
39+
deletePluginDirectories = false, overwritePlugins = false, loadIncompatiblePlugins = true
40+
}
41+
var pluginObj = $pluginObj(config)
42+
expect(pluginObj.$folders().recordCount).toBe(0)
43+
})
44+
45+
it("$files() returns an empty query for a missing directory", () => {
46+
var config = {
47+
path = "wheels", fileName = "Plugins", method = "$init", pluginPath = missingPath,
48+
deletePluginDirectories = false, overwritePlugins = false, loadIncompatiblePlugins = true
49+
}
50+
var pluginObj = $pluginObj(config)
51+
expect(pluginObj.$files().recordCount).toBe(0)
52+
})
53+
})
54+
}
55+
56+
// Mirror the sibling plugin specs (pluginsSpec.cfc:549, pluginsModernSpec,
57+
// pluginsSemverSpec, pluginsManifestIntegrationSpec): a component-level
58+
// helper that instantiates wheels.Plugins via $createObjectFromRoot and
59+
// dispatches $init with the full config — INCLUDING pluginPath. Without it,
60+
// $pluginObj(config) resolves to the parameterless Global.$pluginObj() that
61+
// WheelsTest auto-binds, which ignores config and returns the cached PluginObj
62+
// pointing at the real plugins/ dir — so the missing-path branch (the fix)
63+
// never runs and these specs pass for the wrong reason.
64+
function $pluginObj(required struct config) {
65+
return g.$createObjectFromRoot(argumentCollection = arguments.config)
66+
}
67+
}

0 commit comments

Comments
 (0)