From 632490d71b34a33c258f982164064d76a0592f36 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 4 Nov 2026 11:42:56 -0600 Subject: [PATCH 1/5] Export the control panel utilities plugin bundles need --- CHANGELOG-WIP.md | 1 + resources/js/cp-module.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 76e46d9a91e..310a3d08f94 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -854,6 +854,7 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/ - Removed the `Craft.FocalPoint` and `Craft.Updater` control panel JavaScript classes. The image preview and the updater now start themselves from their markup. - `craft\web\assets\focalpoint\FocalPointAsset` now does nothing. - Added `AuthChallengeForm`, `AssetSelectorController`, `ElementSelectorController`, `Uploads`, `createElementSelectorModal`, `highlighter`, and `registerElementSelectorController` to the `@craftcms/cp` import-map module. Control panel plugin code should import them from there; they’re no longer `Craft.*` globals, except `Craft.createElementSelectorModal` and `Craft.highlighter`, which the Yii2 adapter still provides. +- Added `t()`, `escapeHtml()`, `appendHeadHtml()`, `appendBodyHtml()`, `appendElementHtml()`, `actionClient`, `http`, `getUrl()`, `getCpUrl()`, `getActionUrl()`, `openSlideout()`, `createElementEditor()`, `addActionsToChip()`, `loadElementThumbs()`, and `openAssetImageEditor()` to the `@craftcms/cp` import-map module, replacing the `Craft.*` globals of the same names. Importing them from there keeps a plugin bundle on the control panel’s own instances rather than bundling second copies. - Removed the classic (pre-3.2) `Craft.LivePreview` control panel JavaScript class, which Craft hasn’t used since 3.2. ([#19964](https://github.com/craftcms/cms/pull/19964)) - Deprecated the `Craft.LightSwitch`, `Craft.InfoIcon`, `Craft.ColorInput`, `Craft.PasswordInput`, `Craft.IconPicker`, `Craft.SlidePicker`, `Craft.SlideRuleInput`, and `Craft.Tooltip` control panel JavaScript classes, along with the `.infoicon` jQuery plugin. The corresponding `@craftcms/ui` web components should be used instead. - Deprecated the `Craft.SortableCheckboxSelect` control panel JavaScript class. `` should be used instead. ([#20036](https://github.com/craftcms/cms/pull/20036)) diff --git a/resources/js/cp-module.ts b/resources/js/cp-module.ts index 3764adb5599..6092f2de587 100644 --- a/resources/js/cp-module.ts +++ b/resources/js/cp-module.ts @@ -192,3 +192,28 @@ export const CommandPalette = { export type * from './modules/command-palette/types'; export type {CommandPaletteRegistration} from './modules/command-palette/registry'; + +/** + * The utilities a plugin's own bundle needs to behave like control panel code: + * translating, talking to controller actions, and appending markup the server + * returned. They're re-exported here, rather than left to be imported from + * `@craftcms/ui`, so a plugin bundle shares the control panel's instances + * instead of bundling second copies. + */ +export {t} from '@craftcms/ui/utilities/translate'; +export {escapeHtml} from '@craftcms/ui/utilities/escapeHtml'; +export { + appendBodyHtml, + appendElementHtml, + appendHeadHtml, + type AppendHtmlDisposer, +} from '@craftcms/ui/utilities/dom'; +export {actionClient, http} from '@craftcms/ui'; +export {getActionUrl, getCpUrl, getUrl} from './common/craft/url'; + +/** Slideouts, element editors, chip actions and thumbnails. */ +export {openSlideout, createElementEditor} from './common/slideouts'; +export type {ElementEditorPanel} from './common/slideouts'; +export {addActionsToChip, type ChipAction} from './common/craft/chip-actions'; +export {loadElementThumbs} from './modules/element-thumb-loader'; +export {openAssetImageEditor} from './modules/image-editor/open-asset-image-editor'; From d39a02369b0fc90eb8a288477fe6420791cfc75f Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 4 Nov 2026 12:11:33 -0600 Subject: [PATCH 2/5] Export the control panel config from @craftcms/cp --- CHANGELOG-WIP.md | 2 +- resources/js/cp-module.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 310a3d08f94..e16a9b9acd8 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -854,7 +854,7 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/ - Removed the `Craft.FocalPoint` and `Craft.Updater` control panel JavaScript classes. The image preview and the updater now start themselves from their markup. - `craft\web\assets\focalpoint\FocalPointAsset` now does nothing. - Added `AuthChallengeForm`, `AssetSelectorController`, `ElementSelectorController`, `Uploads`, `createElementSelectorModal`, `highlighter`, and `registerElementSelectorController` to the `@craftcms/cp` import-map module. Control panel plugin code should import them from there; they’re no longer `Craft.*` globals, except `Craft.createElementSelectorModal` and `Craft.highlighter`, which the Yii2 adapter still provides. -- Added `t()`, `escapeHtml()`, `appendHeadHtml()`, `appendBodyHtml()`, `appendElementHtml()`, `actionClient`, `http`, `getUrl()`, `getCpUrl()`, `getActionUrl()`, `openSlideout()`, `createElementEditor()`, `addActionsToChip()`, `loadElementThumbs()`, and `openAssetImageEditor()` to the `@craftcms/cp` import-map module, replacing the `Craft.*` globals of the same names. Importing them from there keeps a plugin bundle on the control panel’s own instances rather than bundling second copies. +- Added `t()`, `escapeHtml()`, `appendHeadHtml()`, `appendBodyHtml()`, `appendElementHtml()`, `actionClient`, `http`, `getUrl()`, `getCpUrl()`, `getActionUrl()`, `cpConfig()`, `openSlideout()`, `createElementEditor()`, `addActionsToChip()`, `loadElementThumbs()`, and `openAssetImageEditor()` to the `@craftcms/cp` import-map module, replacing the `Craft.*` globals of the same names. Importing them from there keeps a plugin bundle on the control panel’s own instances rather than bundling second copies. - Removed the classic (pre-3.2) `Craft.LivePreview` control panel JavaScript class, which Craft hasn’t used since 3.2. ([#19964](https://github.com/craftcms/cms/pull/19964)) - Deprecated the `Craft.LightSwitch`, `Craft.InfoIcon`, `Craft.ColorInput`, `Craft.PasswordInput`, `Craft.IconPicker`, `Craft.SlidePicker`, `Craft.SlideRuleInput`, and `Craft.Tooltip` control panel JavaScript classes, along with the `.infoicon` jQuery plugin. The corresponding `@craftcms/ui` web components should be used instead. - Deprecated the `Craft.SortableCheckboxSelect` control panel JavaScript class. `` should be used instead. ([#20036](https://github.com/craftcms/cms/pull/20036)) diff --git a/resources/js/cp-module.ts b/resources/js/cp-module.ts index 6092f2de587..1d86c902d83 100644 --- a/resources/js/cp-module.ts +++ b/resources/js/cp-module.ts @@ -1,4 +1,5 @@ import {defineAsyncComponent} from 'vue'; +import {ConfigService} from '@craftcms/ui/services/Config'; import {FileUpload, UploadError, registerTransport} from './upload-client'; import { closeCommandPalette, @@ -211,6 +212,15 @@ export { export {actionClient, http} from '@craftcms/ui'; export {getActionUrl, getCpUrl, getUrl} from './common/craft/url'; +/** + * The control panel's own config — `siteId`, `sites`, `userIsAdmin`, + * `elementTypeNames`, `orientation` and the rest of the payload the control + * panel boots with. + */ +export function cpConfig(): ConfigService { + return ConfigService.getInstance(); +} + /** Slideouts, element editors, chip actions and thumbnails. */ export {openSlideout, createElementEditor} from './common/slideouts'; export type {ElementEditorPanel} from './common/slideouts'; From 7c4b4d634a9d4669ed24caa7dc7a71673c481f61 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 4 Nov 2026 12:18:06 -0600 Subject: [PATCH 3/5] Export Uploader from @craftcms/cp --- CHANGELOG-WIP.md | 2 +- resources/js/cp-module.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index e16a9b9acd8..f2c5e821c88 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -854,7 +854,7 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/ - Removed the `Craft.FocalPoint` and `Craft.Updater` control panel JavaScript classes. The image preview and the updater now start themselves from their markup. - `craft\web\assets\focalpoint\FocalPointAsset` now does nothing. - Added `AuthChallengeForm`, `AssetSelectorController`, `ElementSelectorController`, `Uploads`, `createElementSelectorModal`, `highlighter`, and `registerElementSelectorController` to the `@craftcms/cp` import-map module. Control panel plugin code should import them from there; they’re no longer `Craft.*` globals, except `Craft.createElementSelectorModal` and `Craft.highlighter`, which the Yii2 adapter still provides. -- Added `t()`, `escapeHtml()`, `appendHeadHtml()`, `appendBodyHtml()`, `appendElementHtml()`, `actionClient`, `http`, `getUrl()`, `getCpUrl()`, `getActionUrl()`, `cpConfig()`, `openSlideout()`, `createElementEditor()`, `addActionsToChip()`, `loadElementThumbs()`, and `openAssetImageEditor()` to the `@craftcms/cp` import-map module, replacing the `Craft.*` globals of the same names. Importing them from there keeps a plugin bundle on the control panel’s own instances rather than bundling second copies. +- Added `t()`, `escapeHtml()`, `appendHeadHtml()`, `appendBodyHtml()`, `appendElementHtml()`, `actionClient`, `http`, `getUrl()`, `getCpUrl()`, `getActionUrl()`, `cpConfig()`, `openSlideout()`, `createElementEditor()`, `addActionsToChip()`, `loadElementThumbs()`, `openAssetImageEditor()`, and `Uploader` to the `@craftcms/cp` import-map module, replacing the `Craft.*` globals of the same names. Importing them from there keeps a plugin bundle on the control panel’s own instances rather than bundling second copies. - Removed the classic (pre-3.2) `Craft.LivePreview` control panel JavaScript class, which Craft hasn’t used since 3.2. ([#19964](https://github.com/craftcms/cms/pull/19964)) - Deprecated the `Craft.LightSwitch`, `Craft.InfoIcon`, `Craft.ColorInput`, `Craft.PasswordInput`, `Craft.IconPicker`, `Craft.SlidePicker`, `Craft.SlideRuleInput`, and `Craft.Tooltip` control panel JavaScript classes, along with the `.infoicon` jQuery plugin. The corresponding `@craftcms/ui` web components should be used instead. - Deprecated the `Craft.SortableCheckboxSelect` control panel JavaScript class. `` should be used instead. ([#20036](https://github.com/craftcms/cms/pull/20036)) diff --git a/resources/js/cp-module.ts b/resources/js/cp-module.ts index 1d86c902d83..afd4c828aa1 100644 --- a/resources/js/cp-module.ts +++ b/resources/js/cp-module.ts @@ -227,3 +227,4 @@ export type {ElementEditorPanel} from './common/slideouts'; export {addActionsToChip, type ChipAction} from './common/craft/chip-actions'; export {loadElementThumbs} from './modules/element-thumb-loader'; export {openAssetImageEditor} from './modules/image-editor/open-asset-image-editor'; +export {Uploader} from './modules/uploader/uploader'; From d72076d4f76774591d6c50bf2e54c33f3f0f0c19 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 4 Nov 2026 12:29:22 -0600 Subject: [PATCH 4/5] Publish @craftcms/garnish through the control panel import map --- CHANGELOG-WIP.md | 1 + resources/js/garnish.ts | 6 ++++++ src/Cp/Cp.php | 1 + 3 files changed, 8 insertions(+) create mode 100644 resources/js/garnish.ts diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index f2c5e821c88..5daaa9d3ff1 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -855,6 +855,7 @@ Craft 6 now uses [Laravel's authorization system](https://laravel.com/docs/12.x/ - `craft\web\assets\focalpoint\FocalPointAsset` now does nothing. - Added `AuthChallengeForm`, `AssetSelectorController`, `ElementSelectorController`, `Uploads`, `createElementSelectorModal`, `highlighter`, and `registerElementSelectorController` to the `@craftcms/cp` import-map module. Control panel plugin code should import them from there; they’re no longer `Craft.*` globals, except `Craft.createElementSelectorModal` and `Craft.highlighter`, which the Yii2 adapter still provides. - Added `t()`, `escapeHtml()`, `appendHeadHtml()`, `appendBodyHtml()`, `appendElementHtml()`, `actionClient`, `http`, `getUrl()`, `getCpUrl()`, `getActionUrl()`, `cpConfig()`, `openSlideout()`, `createElementEditor()`, `addActionsToChip()`, `loadElementThumbs()`, `openAssetImageEditor()`, and `Uploader` to the `@craftcms/cp` import-map module, replacing the `Craft.*` globals of the same names. Importing them from there keeps a plugin bundle on the control panel’s own instances rather than bundling second copies. +- The control panel's import map now publishes `@craftcms/garnish`, so plugin bundles can import its drag-and-drop, modals and menus rather than relying on the Yii2 adapter's `window.Garnish`. - Removed the classic (pre-3.2) `Craft.LivePreview` control panel JavaScript class, which Craft hasn’t used since 3.2. ([#19964](https://github.com/craftcms/cms/pull/19964)) - Deprecated the `Craft.LightSwitch`, `Craft.InfoIcon`, `Craft.ColorInput`, `Craft.PasswordInput`, `Craft.IconPicker`, `Craft.SlidePicker`, `Craft.SlideRuleInput`, and `Craft.Tooltip` control panel JavaScript classes, along with the `.infoicon` jQuery plugin. The corresponding `@craftcms/ui` web components should be used instead. - Deprecated the `Craft.SortableCheckboxSelect` control panel JavaScript class. `` should be used instead. ([#20036](https://github.com/craftcms/cms/pull/20036)) diff --git a/resources/js/garnish.ts b/resources/js/garnish.ts new file mode 100644 index 00000000000..d597b4ee229 --- /dev/null +++ b/resources/js/garnish.ts @@ -0,0 +1,6 @@ +/** + * Garnish, published to plugin bundles through the import map (see + * `Cp::sharedModules()`) so a plugin's drag-and-drop, modals and menus share + * the control panel's instance rather than bundling their own copy. + */ +export * from '@craftcms/garnish'; diff --git a/src/Cp/Cp.php b/src/Cp/Cp.php index 35ffc5efe46..3cfccef5f47 100644 --- a/src/Cp/Cp.php +++ b/src/Cp/Cp.php @@ -254,6 +254,7 @@ public static function sharedModules(): array return [ 'vue' => static::vite()->asset('resources/js/vue.ts'), '@craftcms/cp' => static::vite()->asset('resources/js/cp-module.ts'), + '@craftcms/garnish' => static::vite()->asset('resources/js/garnish.ts'), ]; } From c946ef9361a8c0c0b7b6e4d34a44f9039f68103a Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 4 Nov 2026 14:47:18 -0600 Subject: [PATCH 5/5] Register garnish.ts as a Vite entry point Without it the module never lands in the manifest, so resolving the import map throws outside the dev server. --- tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php | 5 +++-- vite.config.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php b/tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php index 74073ef4b71..9f4f1d0e0e3 100644 --- a/tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php +++ b/tests/Unit/Http/Middleware/HandleInertiaRequestsTest.php @@ -32,13 +32,14 @@ File::deleteDirectory($publicPath); File::ensureDirectoryExists("{$publicPath}/vendor/craft/build"); - File::put("{$publicPath}/vendor/craft/build/manifest.json", '{"resources/js/vue.ts":{"file":"assets/vue-abc123.js","isEntry":true},"resources/js/cp-module.ts":{"file":"assets/cp-module-def456.js","isEntry":true}}'); + File::put("{$publicPath}/vendor/craft/build/manifest.json", '{"resources/js/vue.ts":{"file":"assets/vue-abc123.js","isEntry":true},"resources/js/cp-module.ts":{"file":"assets/cp-module-def456.js","isEntry":true},"resources/js/garnish.ts":{"file":"assets/garnish-ghi789.js","isEntry":true}}'); app()->usePublicPath($publicPath); try { expect(Cp::sharedModules())->toHaveKey('vue') ->and(Cp::sharedModules()['vue'])->toEndWith('/vendor/craft/build/assets/vue-abc123.js') - ->and(Cp::sharedModules()['@craftcms/cp'])->toEndWith('/vendor/craft/build/assets/cp-module-def456.js'); + ->and(Cp::sharedModules()['@craftcms/cp'])->toEndWith('/vendor/craft/build/assets/cp-module-def456.js') + ->and(Cp::sharedModules()['@craftcms/garnish'])->toEndWith('/vendor/craft/build/assets/garnish-ghi789.js'); } finally { app()->usePublicPath($originalPublicPath); File::deleteDirectory($publicPath); diff --git a/vite.config.js b/vite.config.js index 05d7ce0b26c..4755583634a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -401,8 +401,8 @@ export default defineConfig(async ({mode}) => { emptyOutDir: true, rollupOptions: { // App builds drop entry exports by default, which would empty the - // `vue.ts`/`cp-module.ts` entries that plugin bundles import through - // the import map. + // `vue.ts`/`cp-module.ts`/`garnish.ts` entries that plugin bundles + // import through the import map. preserveEntrySignatures: 'exports-only', }, }, @@ -482,6 +482,7 @@ export default defineConfig(async ({mode}) => { 'yii2-adapter/resources/js/cp-compat.ts', 'yii2-adapter/resources/js/element-editor.ts', 'resources/js/cp-module.ts', + 'resources/js/garnish.ts', 'resources/js/legacy.ts', 'resources/js/uploads.ts', 'resources/js/vue.ts',