mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
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 <rijkvanzanten@me.com> Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0859102a61
commit
7bf90efa62
@@ -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<string>);
|
||||
|
||||
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);
|
||||
})
|
||||
|
||||
@@ -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<Record<AppExtensionType | HybridExtensionType, string>>;
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
const sharedDepsMapping = await this.getSharedDepsMapping(APP_SHARED_DEPS);
|
||||
const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
|
||||
find: name,
|
||||
replacement: path,
|
||||
}));
|
||||
|
||||
const bundles: Partial<Record<AppExtensionType | HybridExtensionType, string>> = {};
|
||||
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<Record<string, string>> {
|
||||
const appDir = await fse.readdir(path.join(resolvePackage('@directus/app', __dirname), 'dist', 'assets'));
|
||||
|
||||
const depsMapping: Record<string, string> = {};
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user