Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -4099,7 +4099,7 @@ component extends="modules.BaseModule" {
pattern: "",
checkType: "directory",
path: "app/plugins",
fix: "Migrate to packages/ + vendor/ activation model"
fix: "Migrate plugins to packages installed under vendor/ (wheels packages add <name>)"
});
arrayAppend(checks, {
description: "Old test base class (wheels.Test)",
Expand All @@ -4114,11 +4114,11 @@ component extends="modules.BaseModule" {
// 3.x -> 4.x
if (currentMajor <= 3 && targetMajor >= 4) {
arrayAppend(checks, {
description: "Legacy plugin directory (deprecated in 4.x)",
description: "Legacy plugin directory (deprecated as of 4.0, removed in 5.0)",
pattern: "",
checkType: "directory",
path: "plugins",
fix: "Migrate to packages/ + vendor/ system"
fix: "Migrate plugins to packages installed under vendor/ (wheels packages add <name>)"
});
arrayAppend(checks, {
description: "Old test base class (wheels.Test)",
Expand Down
42 changes: 42 additions & 0 deletions vendor/wheels/Global.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -2950,6 +2950,48 @@ return local.$wheels;
return local.rv;
}

/**
* Internal function. Records a deprecation warning through a single shared
* policy: the first call for a given feature logs a warning to the standard
* wheels log and registers the warning in
* application[appKey].deprecationWarnings so running apps can surface it
* (debug panel, tooling). Subsequent calls for the same feature are no-ops,
* making the helper safe to call from per-request code paths.
*
* @feature Stable identifier for the deprecated feature (e.g. "plugins-directory", "paginationLinks").
* @message Human-readable message: what is deprecated, what replaces it, and when it goes away.
* @docUrl Optional URL of the migration guide, appended to the logged message.
*/
public void function $deprecated(required string feature, required string message, string docUrl = "") {
try {
local.appKey = $appKey();
if (StructKeyExists(application, local.appKey)) {
if (!StructKeyExists(application[local.appKey], "deprecationWarnings")) {
application[local.appKey].deprecationWarnings = [];
}
for (local.existing in application[local.appKey].deprecationWarnings) {
if (local.existing.feature == arguments.feature) {
return;
}
}
ArrayAppend(application[local.appKey].deprecationWarnings, {
feature = arguments.feature,
message = arguments.message,
url = arguments.docUrl
});
}
} catch (any e) {
// Registration is best-effort; never let a deprecation notice break the caller.
}
local.text = "[Wheels] Deprecation: " & arguments.message;
if (Len(arguments.docUrl)) {
local.text &= " See: " & arguments.docUrl;
}
try {
WriteLog(type = "warning", text = local.text, file = "wheels");
} catch (any e) {}
}

// Returns the running framework version. Delegates to BuildInfo.cfc, which
// is the authoritative version source. The historical box.json-reading
// implementation (with monorepo / wheels-base-template fallback chain)
Expand Down
52 changes: 36 additions & 16 deletions vendor/wheels/Plugins.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ component output="false" extends="wheels.Global"{
$processMixins();
/* dependencies */
$determineDependency();
/* deprecation warning: plugins/ directory is deprecated in favor of packages/ */
/* deprecation warning: plugins/ directory is deprecated in favor of packages installed in vendor/ */
$checkPluginsDeprecation();
return this;
}
Expand Down Expand Up @@ -143,7 +143,7 @@ component output="false" extends="wheels.Global"{
WriteLog(
text = "[Wheels] Loading plugin '#local.pluginKey#' from #local.pluginValue.folderPath#",
type = "information",
file = "wheels_security"
file = "wheels"
);
} catch (any e) {}
local.plugin = CreateObject("component", $componentPathToPlugin(local.pluginKey, local.pluginValue.name)).init();
Expand All @@ -168,12 +168,16 @@ component output="false" extends="wheels.Global"{
&& !$isServiceProvider(local.plugin)
&& !$hasPluginManifest(local.pluginKey)
) {
local.warning = 'Plugin "#local.pluginKey#" uses legacy mixin injection without a plugin.json manifest or ServiceProvider.cfc. Mixin-only plugins will be deprecated in Wheels 4.0. See: https://guides.wheels.dev/docs/migrating-plugins-to-service-providers';
local.warning = 'Plugin "#local.pluginKey#" uses legacy mixin injection without a plugin.json manifest or ServiceProvider.cfc. Legacy plugins are deprecated as of Wheels 4.0 and will be removed in Wheels 5.0 — migrate it to a package installed under vendor/.';
ArrayAppend(variables.$class.deprecationWarnings, {
plugin = local.pluginKey,
message = local.warning
});
WriteLog(type="warning", text="[Wheels] #local.warning#");
$deprecated(
feature = "plugins:mixin-only:#local.pluginKey#",
message = local.warning,
docUrl = "https://guides.wheels.dev/v4-0-0/upgrading/3x-to-4x/"
);
}
if ($isVersionMismatch(local.compatVersion, local.wheelsVersion)) {
variables.$class.incompatiblePlugins = ListAppend(variables.$class.incompatiblePlugins, local.pluginKey);
Expand Down Expand Up @@ -243,7 +247,7 @@ component output="false" extends="wheels.Global"{
// Log an info-level suggestion so authors know about the new manifest option.
WriteLog(
type = "information",
text = "[Wheels] Plugin '#local.plugin#' does not have a plugin.json manifest. Consider adding one for declarative metadata, dependency management, and middleware registration. See: https://guides.wheels.dev/docs/plugin-json-manifest"
text = "[Wheels] Plugin '#local.plugin#' does not have a plugin.json manifest. Consider adding one for declarative metadata, dependency management, and middleware registration — or migrate the plugin to a package installed under vendor/. See: https://guides.wheels.dev/v4-0-0/digging-deeper/packages/"
);
}
}
Expand Down Expand Up @@ -600,15 +604,17 @@ component output="false" extends="wheels.Global"{
}

/**
* Logs a deprecation warning if the plugins/ directory contains any loaded plugins.
* The plugins/ directory is deprecated in favor of the packages/ system.
* Records a deprecation warning if the plugins/ directory contains any loaded plugins.
* The plugins/ directory is deprecated in favor of the package system (packages are
* installed directly into vendor/).
*/
private void function $checkPluginsDeprecation() {
if (!StructIsEmpty(variables.$class.plugins)) {
local.pluginList = StructKeyList(variables.$class.plugins);
WriteLog(
type = "warning",
text = "[Wheels] The plugins/ directory is deprecated as of Wheels 4.0 and will be removed in Wheels 5.0. Plugins found: #local.pluginList#. Move them to packages/ and activate by copying to vendor/. See: https://wheels.dev/docs/packages"
$deprecated(
feature = "plugins-directory",
message = "The plugins/ directory is deprecated as of Wheels 4.0 and will be removed in Wheels 5.0. Plugins found: #local.pluginList#. Migrate each one to a package installed under vendor/ (`wheels packages add <name>` for published packages).",
docUrl = "https://guides.wheels.dev/v4-0-0/digging-deeper/packages/"
);
}
}
Expand Down Expand Up @@ -709,11 +715,21 @@ component output="false" extends="wheels.Global"{
if (!StructKeyExists(arguments.plugin, "onPluginLoad") || !IsCustomFunction(arguments.plugin.onPluginLoad)) {
return;
}
local.loadContext = Duplicate(application);
// Shallow copy: the Adobe CF workaround only requires a plain struct
// context (the application scope itself rejects function members), not
// a deep clone. Shared keys keep referencing the live objects (DI
// container, config struct, framework instance) so nothing forks, and
// the per-plugin cost is O(top-level keys) instead of a deep copy of
// the entire application scope.
local.loadContext = StructCopy(application);
$installPluginLoadAPI(arguments.pluginKey, local.loadContext);
arguments.plugin.onPluginLoad(local.loadContext);
// Sync non-function keys back to application scope. Closures injected
// by $installPluginLoadAPI are skipped to keep application clean.
// Sync non-function keys back to the application scope. Closures
// injected by $installPluginLoadAPI are skipped to keep application
// clean. For shared keys this re-assigns the same reference (a no-op);
// the loop matters for keys the plugin added or replaced, and for
// arrays on Adobe CF, which copies arrays by value even in a shallow
// StructCopy.
for (local.contextKey in local.loadContext) {
if (!IsCustomFunction(local.loadContext[local.contextKey])) {
application[local.contextKey] = local.loadContext[local.contextKey];
Expand Down Expand Up @@ -847,13 +863,17 @@ component output="false" extends="wheels.Global"{

if (IsDefined("application") && !StructIsEmpty(application[$wheels.appKey].mixins)) {
$wheels.metaData = GetMetadata(variablesScope.this);
// Classify by dotted-path segment, not unanchored substring: an
// unanchored FindNoCase("controllers", ...) also matched component
// names like "app.models.ControllerStats" and handed them the
// controller mixin set (di-packages:12).
if (StructKeyExists($wheels.metaData, "displayName")) {
$wheels.className = $wheels.metaData.displayName;
} else if (findNoCase("controllers", $wheels.metaData.fullname)){
} else if (ListFindNoCase($wheels.metaData.fullname, "controllers", "./\")) {
$wheels.className = "controller";
} else if (findNoCase("models", $wheels.metaData.fullname)){
} else if (ListFindNoCase($wheels.metaData.fullname, "models", "./\")) {
$wheels.className = "model";
} else if (findNoCase("tests", $wheels.metaData.fullname)){
} else if (ListFindNoCase($wheels.metaData.fullname, "tests", "./\")) {
$wheels.className = "test";
} else {
$wheels.className = Reverse(SpanExcluding(Reverse($wheels.metaData.name), "."));
Expand Down
16 changes: 16 additions & 0 deletions vendor/wheels/events/onrequestend/debug.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,22 @@ OR (StructKeyExists(url, "format") AND ListFindNoCase("json,xml,csv,pdf", url.fo
</div>
</cfif>
</cfif>
<!--- Deprecation warnings collected via the shared $deprecated() helper --->
<cfif StructKeyExists(application.wheels, "deprecationWarnings") AND ArrayLen(application.wheels.deprecationWarnings)>
<div class="wdb-section">
<div class="wdb-section-title" style="color:##f9e2af;">Deprecations</div>
<div style="color:##f9e2af;font-size:12px;">
<cfloop array="#application.wheels.deprecationWarnings#" index="local.dw">
<p>
#EncodeForHTML(local.dw.message)#
<cfif StructKeyExists(local.dw, "url") AND Len(local.dw.url)>
<a href="#EncodeForHTMLAttribute(local.dw.url)#" style="color:##89b4fa;" target="_blank" rel="noopener">Migration guide</a>
</cfif>
</p>
</cfloop>
</div>
</div>
</cfif>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Fixture for pluginsMixinClassificationSpec (di-packages:12): a component under
// a real "controllers" path segment — must keep classifying as a controller.
component {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Fixture for pluginsMixinClassificationSpec (di-packages:12): a component that
// lives under a "models" path segment but whose name starts with "ControllerS",
// so an unanchored FindNoCase("controllers", fullname) misclassifies it as a
// controller. Dotted-segment matching must classify it as a model.
component {
}
65 changes: 65 additions & 0 deletions vendor/wheels/tests/specs/global/deprecatedHelperSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// upgrade-docs:6 — shared $deprecated() helper: one policy for logging and
// registering deprecation warnings so they are visible to running apps
// (application[appKey].deprecationWarnings, rendered by the debug panel).
component extends="wheels.WheelsTest" {

function run() {

g = application.wo

describe("$deprecated shared helper", () => {

beforeEach(() => {
hadOriginalWarnings = StructKeyExists(application.wheels, "deprecationWarnings")
if (hadOriginalWarnings) {
originalWarnings = application.wheels.deprecationWarnings
}
application.wheels.deprecationWarnings = []
})

afterEach(() => {
if (hadOriginalWarnings) {
application.wheels.deprecationWarnings = originalWarnings
} else {
StructDelete(application.wheels, "deprecationWarnings")
}
})

it("records feature, message and url in the application registry", () => {
g.$deprecated(
feature = "wheelstest-probe",
message = "Probe message.",
docUrl = "https://example.com/migrate"
)
expect(ArrayLen(application.wheels.deprecationWarnings)).toBe(1)
var entry = application.wheels.deprecationWarnings[1]
expect(entry.feature).toBe("wheelstest-probe")
expect(entry.message).toBe("Probe message.")
expect(entry.url).toBe("https://example.com/migrate")
})

it("registers a feature only once per application", () => {
g.$deprecated(feature = "wheelstest-dedupe", message = "First.")
g.$deprecated(feature = "wheelstest-dedupe", message = "Second.")
expect(ArrayLen(application.wheels.deprecationWarnings)).toBe(1)
expect(application.wheels.deprecationWarnings[1].message).toBe("First.")
})

it("registers distinct features separately", () => {
g.$deprecated(feature = "wheelstest-a", message = "A.")
g.$deprecated(feature = "wheelstest-b", message = "B.")
expect(ArrayLen(application.wheels.deprecationWarnings)).toBe(2)
})

it("creates the registry lazily when it does not exist yet", () => {
StructDelete(application.wheels, "deprecationWarnings")
g.$deprecated(feature = "wheelstest-lazy", message = "Lazy.")
expect(StructKeyExists(application.wheels, "deprecationWarnings")).toBeTrue()
expect(ArrayLen(application.wheels.deprecationWarnings)).toBe(1)
})

})

}

}
60 changes: 60 additions & 0 deletions vendor/wheels/tests/specs/pluginsDeprecationMessagingSpec.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Review follow-ups di-packages:7, upgrade-docs:2 and upgrade-docs:3: keep the
// routine plugin-load trace out of the security log, keep the deprecation
// messages pointing at live versioned guide URLs, and keep the remediation text
// aligned with the shipped package system (packages install into vendor/ —
// there is no packages/ staging directory).
component extends="wheels.WheelsTest" {

function run() {

describe("Plugins.cfc deprecation messaging and log routing", () => {

it("routes the plugin-load trace to the standard wheels log, not the security log", () => {
var source = FileRead(ExpandPath("/wheels/Plugins.cfc"));
expect(FindNoCase("wheels_security", source) GT 0).toBeFalse(
"Plugins.cfc must not write routine plugin-load entries to the "
& "wheels_security log — startup noise pollutes the security "
& "audit trail. Use file=""wheels"" like PackageLoader.cfc "
& "(di-packages:7)."
);
});

it("does not link to dead documentation URLs", () => {
var source = FileRead(ExpandPath("/wheels/Plugins.cfc"));
expect(FindNoCase("guides.wheels.dev/docs/", source) GT 0).toBeFalse(
"Plugins.cfc must not link unversioned guides.wheels.dev/docs/ "
& "paths — guides URLs are versioned (e.g. "
& "guides.wheels.dev/v4-0-0/...) and the unversioned forms 404 "
& "(upgrade-docs:2)."
);
expect(FindNoCase("wheels.dev/docs/packages", source) GT 0).toBeFalse(
"Plugins.cfc must not link wheels.dev/docs/packages — the live "
& "page is guides.wheels.dev/v4-0-0/digging-deeper/packages/ "
& "(upgrade-docs:2)."
);
// Positive guard: at least one live versioned guide URL remains.
expect(FindNoCase("https://guides.wheels.dev/v4-0-0/", source) GT 0).toBeTrue(
"Plugins.cfc deprecation messages must point at live versioned "
& "guide URLs (upgrade-docs:2)."
);
});

it("does not describe the abandoned packages/ staging design or use future tense", () => {
var source = FileRead(ExpandPath("/wheels/Plugins.cfc"));
expect(FindNoCase("Move them to packages/", source) GT 0).toBeFalse(
"Plugins.cfc must not tell users to move plugins to a packages/ "
& "staging directory — the shipped loader discovers packages "
& "from vendor/ only (upgrade-docs:3)."
);
expect(FindNoCase("will be deprecated", source) GT 0).toBeFalse(
"Plugins.cfc deprecation text must use present tense — plugins "
& "are deprecated as of Wheels 4.0, not at some future point "
& "(upgrade-docs:3)."
);
});

});

}

}
Loading
Loading