From 7bf90efa6243f60fa4128a82540f2995df2ce9f5 Mon Sep 17 00:00:00 2001 From: Nicola Krumschmidt Date: Wed, 16 Nov 2022 17:28:52 +0100 Subject: [PATCH] Add support for a package extension bundle type (#15672) * Add bundle type to constants and types * Add support for API bundle extensions * Rename generateExtensionsEntry to generateExtensionsEntrypoint * Add support for App bundle extensions * Refactor App extension registration * Replace extensions inject with useExtensions() * Replace getInterfaces() with useExtensions() * Replace getDisplays() with useExtensions() * Replace getLayouts() with useExtensions() * Replace getModules() with useExtensions() * Replace getPanels() with useExtensions() * Replace getOperations() with useExtensions() * Add useExtension() composable * Replace useExtensions() with useExtension() where applicable * Remove interface getters * Remove display getters * Remove layout getters * Remove module getter * Remove panel getters * Remove operation getters * Rename extension register.ts files to index.ts * Perform module pre register check in parallel * Remove Refs from AppExtensionConfigs type * Remove old extension shims * Ensure registration of modules is awaited when hydrating * Add support for scaffolding package extensions * Add support for building bundle extensions * Add JsonValue type * Use json for complex command line flags * Load internal extensions if custom ones are not available * Fix extension manifest validation for pack extensions * Fix tests in shared * Add SplitEntrypoint type * Move command specific utils to helpers * Add SDK version getter * Move extension dev deps generation to helpers * Move template path to getter util * Move template copying to a helper * Only rename copied template files * Add directus-extension add command * Convert provided extension source path to url * Replace deprecated import.meta.globEager * Mock URL.createObjectURL to make App unit tests pass * Update rollup-plugin-typescript2 * indentation * sort vite glob imported modules * fix unintentional wrong commit * Simplify app extension import logic * reinstall @rollup/plugin-virtual * add test for getInterfaces() expected sort order Co-authored-by: Rijk van Zanten Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com> --- api/src/controllers/extensions.ts | 21 +- api/src/extensions.ts | 210 ++++++------ app/src/__setup__/mock-globals.ts | 21 ++ .../v-form/form-field-interface.vue | 9 +- app/src/composables/use-extension.ts | 19 ++ app/src/composables/use-form-fields.ts | 8 +- app/src/composables/use-system.ts | 16 +- app/src/displays/index.ts | 20 +- app/src/displays/register.ts | 34 -- app/src/displays/related-values/index.ts | 8 +- app/src/displays/translations/index.ts | 8 +- app/src/extensions.ts | 99 ++++++ app/src/hydrate.ts | 6 +- .../system-interface-options.vue | 13 +- .../system-interface/system-interface.vue | 4 +- .../_system/system-modules/system-modules.vue | 4 +- app/src/interfaces/index.test.ts | 37 +++ app/src/interfaces/index.ts | 23 +- app/src/interfaces/register.ts | 35 -- app/src/lang/set-language.ts | 20 -- app/src/layouts/index.ts | 19 +- app/src/layouts/register.ts | 34 -- app/src/main.ts | 18 +- app/src/modules/content/routes/collection.vue | 5 +- app/src/modules/index.ts | 62 +++- app/src/modules/insights/routes/dashboard.vue | 4 +- .../insights/routes/panel-configuration.vue | 12 +- app/src/modules/register.ts | 69 ---- .../field-detail-advanced-conditions.vue | 19 +- .../field-detail-advanced-display.vue | 9 +- .../field-detail-advanced-interface.vue | 28 +- .../field-configuration.vue | 7 +- .../field-detail-simple.vue | 4 +- .../field-detail/shared/extension-options.vue | 20 +- .../field-detail/store/alterations/global.ts | 16 +- .../store/alterations/standard.ts | 7 +- .../data-model/field-detail/store/index.ts | 7 +- .../fields/components/field-select.vue | 8 +- .../flows/components/logs-sidebar-detail.vue | 4 +- .../flows/components/operation-detail.vue | 11 +- .../routes/flows/components/operation.vue | 4 +- .../modules/settings/routes/flows/flow.vue | 4 +- .../routes/presets/collection/collection.vue | 5 +- .../modules/settings/routes/presets/item.vue | 4 +- app/src/operations/index.ts | 30 +- app/src/operations/register.ts | 45 --- app/src/panels/index.ts | 20 +- app/src/panels/register.ts | 35 -- app/src/shims.d.ts | 36 +-- app/src/stores/insights.ts | 4 +- app/src/utils/adjust-fields-for-displays.ts | 18 +- app/src/utils/render-string-template.ts | 17 +- app/src/utils/save-as-csv.ts | 19 +- .../components/layout-sidebar-detail.vue | 9 +- .../views/private/components/module-bar.vue | 4 +- .../private/components/render-display.vue | 7 +- .../private/components/render-template.vue | 16 +- app/vite.config.js | 22 +- .../create-directus-extension/lib/index.js | 5 +- packages/extensions-sdk/package.json | 3 + .../extensions-sdk/src/cli/commands/add.ts | 304 ++++++++++++++++++ .../extensions-sdk/src/cli/commands/build.ts | 256 ++++++++++++--- .../extensions-sdk/src/cli/commands/create.ts | 165 ++++++---- .../src/cli/commands/helpers/copy-template.ts | 72 +++++ .../helpers/generate-bundle-entrypoint.ts | 40 +++ .../helpers/get-extension-dev-deps.ts | 32 ++ .../helpers}/load-config.ts | 2 +- .../commands/helpers/validate-cli-options.ts | 48 +++ packages/extensions-sdk/src/cli/run.ts | 5 +- .../src/cli/utils/detect-json-indent.ts | 13 + .../src/cli/utils/get-sdk-version.ts | 5 + .../src/cli/utils/get-template-path.ts | 5 + .../src/cli/utils/rename-map.ts | 20 -- .../extensions-sdk/src/cli/utils/to-object.ts | 16 - .../src/cli/utils/try-parse-json.ts | 9 + .../{javascript => common/config}/_gitignore | 0 .../templates/common/typescript/_gitignore | 3 - .../typescript/{ => config}/tsconfig.json | 0 .../javascript/{src => source}/display.vue | 0 .../javascript/{src => source}/index.js | 0 .../typescript/{src => source}/display.vue | 0 .../typescript/{src => source}/index.ts | 0 .../typescript/{src => source}/shims.d.ts | 0 .../javascript/{src => source}/index.js | 0 .../typescript/{src => source}/index.ts | 0 .../hook/javascript/{src => source}/index.js | 0 .../hook/typescript/{src => source}/index.ts | 0 .../javascript/{src => source}/index.js | 0 .../javascript/{src => source}/interface.vue | 0 .../typescript/{src => source}/index.ts | 0 .../typescript/{src => source}/interface.vue | 0 .../typescript/{src => source}/shims.d.ts | 0 .../javascript/{src => source}/index.js | 0 .../javascript/{src => source}/layout.vue | 0 .../typescript/{src => source}/index.ts | 0 .../typescript/{src => source}/layout.vue | 0 .../typescript/{src => source}/shims.d.ts | 0 .../javascript/{src => source}/index.js | 0 .../javascript/{src => source}/module.vue | 0 .../typescript/{src => source}/index.ts | 0 .../typescript/{src => source}/module.vue | 0 .../typescript/{src => source}/shims.d.ts | 0 .../javascript/{src => source}/api.js | 0 .../javascript/{src => source}/app.js | 0 .../typescript/{src => source}/api.ts | 0 .../typescript/{src => source}/app.ts | 0 .../typescript/{src => source}/shims.d.ts | 0 .../panel/javascript/{src => source}/index.js | 0 .../javascript/{src => source}/panel.vue | 0 .../panel/typescript/{src => source}/index.ts | 0 .../typescript/{src => source}/panel.vue | 0 .../typescript/{src => source}/shims.d.ts | 0 packages/shared/src/composables/use-system.ts | 6 +- packages/shared/src/constants/extensions.ts | 4 +- packages/shared/src/types/extensions.ts | 92 ++++-- packages/shared/src/types/index.ts | 1 + packages/shared/src/types/misc.ts | 2 + packages/shared/src/types/vue.ts | 3 + .../node/generate-extensions-entry.test.ts | 33 -- .../utils/node/generate-extensions-entry.ts | 23 -- .../generate-extensions-entrypoint.test.ts | 129 ++++++++ .../node/generate-extensions-entrypoint.ts | 55 ++++ .../shared/src/utils/node/get-extensions.ts | 23 +- packages/shared/src/utils/node/index.ts | 2 +- .../utils/validate-extension-manifest.test.ts | 103 +++++- .../src/utils/validate-extension-manifest.ts | 84 +++-- pnpm-lock.yaml | 15 + 127 files changed, 1898 insertions(+), 957 deletions(-) create mode 100644 app/src/__setup__/mock-globals.ts create mode 100644 app/src/composables/use-extension.ts delete mode 100644 app/src/displays/register.ts create mode 100644 app/src/extensions.ts create mode 100644 app/src/interfaces/index.test.ts delete mode 100644 app/src/interfaces/register.ts delete mode 100644 app/src/layouts/register.ts delete mode 100644 app/src/modules/register.ts delete mode 100644 app/src/operations/register.ts delete mode 100644 app/src/panels/register.ts create mode 100644 packages/extensions-sdk/src/cli/commands/add.ts create mode 100644 packages/extensions-sdk/src/cli/commands/helpers/copy-template.ts create mode 100644 packages/extensions-sdk/src/cli/commands/helpers/generate-bundle-entrypoint.ts create mode 100644 packages/extensions-sdk/src/cli/commands/helpers/get-extension-dev-deps.ts rename packages/extensions-sdk/src/cli/{utils => commands/helpers}/load-config.ts (94%) create mode 100644 packages/extensions-sdk/src/cli/commands/helpers/validate-cli-options.ts create mode 100644 packages/extensions-sdk/src/cli/utils/detect-json-indent.ts create mode 100644 packages/extensions-sdk/src/cli/utils/get-sdk-version.ts create mode 100644 packages/extensions-sdk/src/cli/utils/get-template-path.ts delete mode 100644 packages/extensions-sdk/src/cli/utils/rename-map.ts delete mode 100644 packages/extensions-sdk/src/cli/utils/to-object.ts create mode 100644 packages/extensions-sdk/src/cli/utils/try-parse-json.ts rename packages/extensions-sdk/templates/common/{javascript => common/config}/_gitignore (100%) delete mode 100644 packages/extensions-sdk/templates/common/typescript/_gitignore rename packages/extensions-sdk/templates/common/typescript/{ => config}/tsconfig.json (100%) rename packages/extensions-sdk/templates/display/javascript/{src => source}/display.vue (100%) rename packages/extensions-sdk/templates/display/javascript/{src => source}/index.js (100%) rename packages/extensions-sdk/templates/display/typescript/{src => source}/display.vue (100%) rename packages/extensions-sdk/templates/display/typescript/{src => source}/index.ts (100%) rename packages/extensions-sdk/templates/display/typescript/{src => source}/shims.d.ts (100%) rename packages/extensions-sdk/templates/endpoint/javascript/{src => source}/index.js (100%) rename packages/extensions-sdk/templates/endpoint/typescript/{src => source}/index.ts (100%) rename packages/extensions-sdk/templates/hook/javascript/{src => source}/index.js (100%) rename packages/extensions-sdk/templates/hook/typescript/{src => source}/index.ts (100%) rename packages/extensions-sdk/templates/interface/javascript/{src => source}/index.js (100%) rename packages/extensions-sdk/templates/interface/javascript/{src => source}/interface.vue (100%) rename packages/extensions-sdk/templates/interface/typescript/{src => source}/index.ts (100%) rename packages/extensions-sdk/templates/interface/typescript/{src => source}/interface.vue (100%) rename packages/extensions-sdk/templates/interface/typescript/{src => source}/shims.d.ts (100%) rename packages/extensions-sdk/templates/layout/javascript/{src => source}/index.js (100%) rename packages/extensions-sdk/templates/layout/javascript/{src => source}/layout.vue (100%) rename packages/extensions-sdk/templates/layout/typescript/{src => source}/index.ts (100%) rename packages/extensions-sdk/templates/layout/typescript/{src => source}/layout.vue (100%) rename packages/extensions-sdk/templates/layout/typescript/{src => source}/shims.d.ts (100%) rename packages/extensions-sdk/templates/module/javascript/{src => source}/index.js (100%) rename packages/extensions-sdk/templates/module/javascript/{src => source}/module.vue (100%) rename packages/extensions-sdk/templates/module/typescript/{src => source}/index.ts (100%) rename packages/extensions-sdk/templates/module/typescript/{src => source}/module.vue (100%) rename packages/extensions-sdk/templates/module/typescript/{src => source}/shims.d.ts (100%) rename packages/extensions-sdk/templates/operation/javascript/{src => source}/api.js (100%) rename packages/extensions-sdk/templates/operation/javascript/{src => source}/app.js (100%) rename packages/extensions-sdk/templates/operation/typescript/{src => source}/api.ts (100%) rename packages/extensions-sdk/templates/operation/typescript/{src => source}/app.ts (100%) rename packages/extensions-sdk/templates/operation/typescript/{src => source}/shims.d.ts (100%) rename packages/extensions-sdk/templates/panel/javascript/{src => source}/index.js (100%) rename packages/extensions-sdk/templates/panel/javascript/{src => source}/panel.vue (100%) rename packages/extensions-sdk/templates/panel/typescript/{src => source}/index.ts (100%) rename packages/extensions-sdk/templates/panel/typescript/{src => source}/panel.vue (100%) rename packages/extensions-sdk/templates/panel/typescript/{src => source}/shims.d.ts (100%) create mode 100644 packages/shared/src/types/vue.ts delete mode 100644 packages/shared/src/utils/node/generate-extensions-entry.test.ts delete mode 100644 packages/shared/src/utils/node/generate-extensions-entry.ts create mode 100644 packages/shared/src/utils/node/generate-extensions-entrypoint.test.ts create mode 100644 packages/shared/src/utils/node/generate-extensions-entrypoint.ts diff --git a/api/src/controllers/extensions.ts b/api/src/controllers/extensions.ts index 4ac36e15e3..83953c2118 100644 --- a/api/src/controllers/extensions.ts +++ b/api/src/controllers/extensions.ts @@ -35,27 +35,20 @@ router.get( ); router.get( - '/:type/index.js', + '/sources/index.js', asyncHandler(async (req, res) => { - const type = depluralize(req.params.type as Plural); - - if (!isIn(type, APP_OR_HYBRID_EXTENSION_TYPES)) { - throw new RouteNotFoundException(req.path); - } - const extensionManager = getExtensionManager(); - const extensionSource = extensionManager.getAppExtensions(type); - if (extensionSource === undefined) { + const extensionSource = extensionManager.getAppExtensions(); + if (extensionSource === null) { throw new RouteNotFoundException(req.path); } res.setHeader('Content-Type', 'application/javascript; charset=UTF-8'); - if (env.EXTENSIONS_CACHE_TTL) { - res.setHeader('Cache-Control', getCacheControlHeader(req, ms(env.EXTENSIONS_CACHE_TTL as string))); - } else { - res.setHeader('Cache-Control', 'no-store'); - } + res.setHeader( + 'Cache-Control', + env.EXTENSIONS_CACHE_TTL ? getCacheControlHeader(req, ms(env.EXTENSIONS_CACHE_TTL as string)) : 'no-store' + ); res.setHeader('Vary', 'Origin, Cache-Control'); res.end(extensionSource); }) diff --git a/api/src/extensions.ts b/api/src/extensions.ts index 4777c0e8fe..a02b4a27af 100644 --- a/api/src/extensions.ts +++ b/api/src/extensions.ts @@ -3,21 +3,20 @@ import path from 'path'; import { ActionHandler, ApiExtension, - AppExtensionType, + BundleExtension, EndpointConfig, Extension, ExtensionType, FilterHandler, HookConfig, HybridExtension, - HybridExtensionType, InitHandler, OperationApiConfig, ScheduleHandler, } from '@directus/shared/types'; import { ensureExtensionDirs, - generateExtensionsEntry, + generateExtensionsEntrypoint, getLocalExtensions, getPackageExtensions, pathToRelativeUrl, @@ -26,12 +25,10 @@ import { import { API_OR_HYBRID_EXTENSION_PACKAGE_TYPES, API_OR_HYBRID_EXTENSION_TYPES, - APP_OR_HYBRID_EXTENSION_TYPES, APP_SHARED_DEPS, EXTENSION_PACKAGE_TYPES, EXTENSION_TYPES, HYBRID_EXTENSION_TYPES, - PACKAGE_EXTENSION_TYPES, } from '@directus/shared/constants'; import getDatabase from './database'; import emitter, { Emitter } from './emitter'; @@ -69,13 +66,15 @@ export function getExtensionManager(): ExtensionManager { return extensionManager; } -type AppExtensions = Partial>; -type ApiExtensions = { - hooks: { path: string; events: EventHandler[] }[]; - endpoints: { path: string }[]; - operations: { path: string }[]; +type BundleConfig = { + endpoints: { name: string; config: EndpointConfig }[]; + hooks: { name: string; config: HookConfig }[]; + operations: { name: string; config: OperationApiConfig }[]; }; +type AppExtensions = string | null; +type ApiExtensions = { path: string }[]; + type Options = { schedule: boolean; watch: boolean; @@ -92,10 +91,11 @@ class ExtensionManager { private extensions: Extension[] = []; - private appExtensions: AppExtensions = {}; - private apiExtensions: ApiExtensions = { hooks: [], endpoints: [], operations: [] }; + private appExtensions: AppExtensions = null; + private apiExtensions: ApiExtensions = []; private apiEmitter: Emitter; + private hookEvents: EventHandler[] = []; private endpointRouter: Router; private reloadQueue: JobQueue; @@ -179,8 +179,8 @@ class ExtensionManager { } } - public getAppExtensions(type: AppExtensionType | HybridExtensionType): string | undefined { - return this.appExtensions[type]; + public getAppExtensions(): string | null { + return this.appExtensions; } public getEndpointRouter(): Router { @@ -198,25 +198,24 @@ class ExtensionManager { } await this.registerHooks(); - this.registerEndpoints(); + await this.registerEndpoints(); await this.registerOperations(); + await this.registerBundles(); if (env.SERVE_APP) { - this.appExtensions = await this.generateExtensionBundles(); + this.appExtensions = await this.generateExtensionBundle(); } this.isLoaded = true; } private async unload(): Promise { - this.unregisterHooks(); - this.unregisterEndpoints(); - this.unregisterOperations(); + this.unregisterApiExtensions(); this.apiEmitter.offAll(); if (env.SERVE_APP) { - this.appExtensions = {}; + this.appExtensions = null; } this.isLoaded = false; @@ -257,9 +256,9 @@ class ExtensionManager { extensions .filter((extension) => !extension.local) .flatMap((extension) => - isTypeIn(extension, PACKAGE_EXTENSION_TYPES) + extension.type === 'pack' ? path.resolve(extension.path, 'package.json') - : isTypeIn(extension, HYBRID_EXTENSION_TYPES) + : isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle' ? [ path.resolve(extension.path, extension.entrypoint.app), path.resolve(extension.path, extension.entrypoint.api), @@ -288,40 +287,36 @@ class ExtensionManager { return [...packageExtensions, ...localExtensions]; } - private async generateExtensionBundles() { + private async generateExtensionBundle(): Promise { const sharedDepsMapping = await this.getSharedDepsMapping(APP_SHARED_DEPS); const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({ find: name, replacement: path, })); - const bundles: Partial> = {}; + const entrypoint = generateExtensionsEntrypoint(this.extensions); - for (const extensionType of APP_OR_HYBRID_EXTENSION_TYPES) { - const entry = generateExtensionsEntry(extensionType, this.extensions); + try { + const bundle = await rollup({ + input: 'entry', + external: Object.values(sharedDepsMapping), + makeAbsoluteExternalsRelative: false, + plugins: [virtual({ entry: entrypoint }), alias({ entries: internalImports })], + }); + const { output } = await bundle.generate({ format: 'es', compact: true }); - try { - const bundle = await rollup({ - input: 'entry', - external: Object.values(sharedDepsMapping), - makeAbsoluteExternalsRelative: false, - plugins: [virtual({ entry }), alias({ entries: internalImports })], - }); - const { output } = await bundle.generate({ format: 'es', compact: true }); + await bundle.close(); - bundles[extensionType] = output[0].code; - - await bundle.close(); - } catch (error: any) { - logger.warn(`Couldn't bundle App extensions`); - logger.warn(error); - } + return output[0].code; + } catch (error: any) { + logger.warn(`Couldn't bundle App extensions`); + logger.warn(error); } - return bundles; + return null; } - private async getSharedDepsMapping(deps: string[]) { + private async getSharedDepsMapping(deps: string[]): Promise> { const appDir = await fse.readdir(path.join(resolvePackage('@directus/app', __dirname), 'dist', 'assets')); const depsMapping: Record = {}; @@ -350,7 +345,9 @@ class ExtensionManager { const config = getModuleDefault(hookInstance); - this.registerHook(config, hookPath); + this.registerHook(config); + + this.apiExtensions.push({ path: hookPath }); } catch (error: any) { logger.warn(`Couldn't register hook "${hook.name}"`); logger.warn(error); @@ -358,7 +355,7 @@ class ExtensionManager { } } - private registerEndpoints(): void { + private async registerEndpoints(): Promise { const endpoints = this.extensions.filter((extension): extension is ApiExtension => extension.type === 'endpoint'); for (const endpoint of endpoints) { @@ -368,7 +365,9 @@ class ExtensionManager { const config = getModuleDefault(endpointInstance); - this.registerEndpoint(config, endpointPath, endpoint.name, this.endpointRouter); + this.registerEndpoint(config, endpoint.name); + + this.apiExtensions.push({ path: endpointPath }); } catch (error: any) { logger.warn(`Couldn't register endpoint "${endpoint.name}"`); logger.warn(error); @@ -400,7 +399,9 @@ class ExtensionManager { const config = getModuleDefault(operationInstance); - this.registerOperation(config, operationPath); + this.registerOperation(config); + + this.apiExtensions.push({ path: operationPath }); } catch (error: any) { logger.warn(`Couldn't register operation "${operation.name}"`); logger.warn(error); @@ -408,17 +409,42 @@ class ExtensionManager { } } - private registerHook(register: HookConfig, path: string) { - const hookHandler: { path: string; events: EventHandler[] } = { - path, - events: [], - }; + private async registerBundles(): Promise { + const bundles = this.extensions.filter((extension): extension is BundleExtension => extension.type === 'bundle'); + for (const bundle of bundles) { + try { + const bundlePath = path.resolve(bundle.path, bundle.entrypoint.api); + const bundleInstances: BundleConfig | { default: BundleConfig } = require(bundlePath); + + const configs = getModuleDefault(bundleInstances); + + for (const { config } of configs.hooks) { + this.registerHook(config); + } + + for (const { config, name } of configs.endpoints) { + this.registerEndpoint(config, name); + } + + for (const { config } of configs.operations) { + this.registerOperation(config); + } + + this.apiExtensions.push({ path: bundlePath }); + } catch (error: any) { + logger.warn(`Couldn't register bundle "${bundle.name}"`); + logger.warn(error); + } + } + } + + private registerHook(register: HookConfig): void { const registerFunctions = { filter: (event: string, handler: FilterHandler) => { emitter.onFilter(event, handler); - hookHandler.events.push({ + this.hookEvents.push({ type: 'filter', name: event, handler, @@ -427,7 +453,7 @@ class ExtensionManager { action: (event: string, handler: ActionHandler) => { emitter.onAction(event, handler); - hookHandler.events.push({ + this.hookEvents.push({ type: 'action', name: event, handler, @@ -436,7 +462,7 @@ class ExtensionManager { init: (event: string, handler: InitHandler) => { emitter.onInit(event, handler); - hookHandler.events.push({ + this.hookEvents.push({ type: 'init', name: event, handler, @@ -454,7 +480,7 @@ class ExtensionManager { } }); - hookHandler.events.push({ + this.hookEvents.push({ type: 'schedule', task, }); @@ -473,16 +499,14 @@ class ExtensionManager { logger, getSchema, }); - - this.apiExtensions.hooks.push(hookHandler); } - private registerEndpoint(config: EndpointConfig, path: string, name: string, router: Router) { + private registerEndpoint(config: EndpointConfig, name: string): void { const register = typeof config === 'function' ? config : config.handler; const routeName = typeof config === 'function' ? name : config.id; const scopedRouter = express.Router(); - router.use(`/${routeName}`, scopedRouter); + this.endpointRouter.use(`/${routeName}`, scopedRouter); register(scopedRouter, { services, @@ -493,66 +517,44 @@ class ExtensionManager { logger, getSchema, }); - - this.apiExtensions.endpoints.push({ - path, - }); } - private registerOperation(config: OperationApiConfig, path: string) { + private registerOperation(config: OperationApiConfig): void { const flowManager = getFlowManager(); flowManager.addOperation(config.id, config.handler); - - this.apiExtensions.operations.push({ - path, - }); } - private unregisterHooks(): void { - for (const hook of this.apiExtensions.hooks) { - for (const event of hook.events) { - switch (event.type) { - case 'filter': - emitter.offFilter(event.name, event.handler); - break; - case 'action': - emitter.offAction(event.name, event.handler); - break; - case 'init': - emitter.offInit(event.name, event.handler); - break; - case 'schedule': - event.task.stop(); - break; - } + private unregisterApiExtensions(): void { + for (const event of this.hookEvents) { + switch (event.type) { + case 'filter': + emitter.offFilter(event.name, event.handler); + break; + case 'action': + emitter.offAction(event.name, event.handler); + break; + case 'init': + emitter.offInit(event.name, event.handler); + break; + case 'schedule': + event.task.stop(); + break; } - - delete require.cache[require.resolve(hook.path)]; } - this.apiExtensions.hooks = []; - } - - private unregisterEndpoints(): void { - for (const endpoint of this.apiExtensions.endpoints) { - delete require.cache[require.resolve(endpoint.path)]; - } + this.hookEvents = []; this.endpointRouter.stack = []; - this.apiExtensions.endpoints = []; - } - - private unregisterOperations(): void { - for (const operation of this.apiExtensions.operations) { - delete require.cache[require.resolve(operation.path)]; - } - const flowManager = getFlowManager(); flowManager.clearOperations(); - this.apiExtensions.operations = []; + for (const apiExtension of this.apiExtensions) { + delete require.cache[require.resolve(apiExtension.path)]; + } + + this.apiExtensions = []; } } diff --git a/app/src/__setup__/mock-globals.ts b/app/src/__setup__/mock-globals.ts new file mode 100644 index 0000000000..9933aa1170 --- /dev/null +++ b/app/src/__setup__/mock-globals.ts @@ -0,0 +1,21 @@ +import { afterEach } from 'vitest'; + +const originals = new Map(); + +originals.set('createObjectURL', globalThis.URL.createObjectURL); + +Object.defineProperty(globalThis.URL, 'createObjectURL', { + value: () => 'blob:http://localhost/0', + writable: true, + enumerable: false, + configurable: true, +}); + +afterEach(() => { + Object.defineProperty(globalThis.URL, 'createObjectURL', { + value: originals.get('createObjectURL'), + writable: true, + enumerable: false, + configurable: true, + }); +}); diff --git a/app/src/components/v-form/form-field-interface.vue b/app/src/components/v-form/form-field-interface.vue index 79646d050c..60a8d8483f 100644 --- a/app/src/components/v-form/form-field-interface.vue +++ b/app/src/components/v-form/form-field-interface.vue @@ -48,8 +48,8 @@ import { useI18n } from 'vue-i18n'; import { computed } from 'vue'; import { Field } from '@directus/shared/types'; -import { getInterface } from '@/interfaces'; import { getDefaultInterfaceForType } from '@/utils/get-default-interface-for-type'; +import { useExtension } from '@/composables/use-extension'; interface Props { field: Field; @@ -82,7 +82,12 @@ defineEmits(['update:modelValue', 'setFieldValue']); const { t } = useI18n(); -const interfaceExists = computed(() => !!getInterface(props.field?.meta?.interface || 'input')); +const inter = useExtension( + 'interface', + computed(() => props.field?.meta?.interface ?? 'input') +); + +const interfaceExists = computed(() => !!inter.value);