mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Extension Improvements (#16822)
* add link command and small improvements * put local bundles into own folder on link * get rid of packs and add bundle support for local extensions * make bundle type extensions work locally and remove traces of pack * fix hot reloading of bundles * fix app.js not refreshing * fixed linter errors * add endpoint to install extensions * update package.json validation and support top level extensions * update endpoints * added some URL escapes and ran linter * remove installation part * readd endpoint * update dependencies * fix types and validation in extension-sdk * run linter * fix linter * add defaults to manifest * Added missing constant export * ensure all the extension folders * ignore unneeded vite error * update linking process * run parser separate * add await * fixed linter errors Co-authored-by: Brainslug <tim@brainslug.nl> Co-authored-by: Brainslug <br41nslug@users.noreply.github.com>
This commit is contained in:
@@ -2,13 +2,13 @@ import { Router } from 'express';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { RouteNotFoundException } from '../exceptions';
|
||||
import { getExtensionManager } from '../extensions';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { depluralize, isIn } from '@directus/shared/utils';
|
||||
import { Plural } from '@directus/shared/types';
|
||||
import { APP_OR_HYBRID_EXTENSION_TYPES } from '@directus/shared/constants';
|
||||
import ms from 'ms';
|
||||
import env from '../env';
|
||||
import { getCacheControlHeader } from '../utils/get-cache-headers';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { depluralize, isIn } from '@directus/shared/utils';
|
||||
import { Plural } from '@directus/shared/types';
|
||||
import { EXTENSION_TYPES } from '@directus/shared/constants';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -17,7 +17,7 @@ router.get(
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const type = depluralize(req.params.type as Plural<string>);
|
||||
|
||||
if (!isIn(type, APP_OR_HYBRID_EXTENSION_TYPES)) {
|
||||
if (!isIn(type, EXTENSION_TYPES)) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
API_OR_HYBRID_EXTENSION_PACKAGE_TYPES,
|
||||
API_OR_HYBRID_EXTENSION_TYPES,
|
||||
APP_EXTENSION_TYPES,
|
||||
APP_SHARED_DEPS,
|
||||
EXTENSION_PACKAGE_TYPES,
|
||||
EXTENSION_TYPES,
|
||||
HYBRID_EXTENSION_TYPES,
|
||||
NESTED_EXTENSION_TYPES,
|
||||
} from '@directus/shared/constants';
|
||||
import * as sharedExceptions from '@directus/shared/exceptions';
|
||||
import {
|
||||
@@ -13,6 +11,7 @@ import {
|
||||
BundleExtension,
|
||||
EndpointConfig,
|
||||
Extension,
|
||||
ExtensionInfo,
|
||||
ExtensionType,
|
||||
FilterHandler,
|
||||
HookConfig,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
getPackageExtensions,
|
||||
pathToRelativeUrl,
|
||||
resolvePackage,
|
||||
resolvePackageExtensions,
|
||||
} from '@directus/shared/utils/node';
|
||||
import express, { Router } from 'express';
|
||||
import fse from 'fs-extra';
|
||||
@@ -133,7 +133,7 @@ class ExtensionManager {
|
||||
|
||||
const loadedExtensions = this.getExtensionsList();
|
||||
if (loadedExtensions.length > 0) {
|
||||
logger.info(`Loaded extensions: ${loadedExtensions.join(', ')}`);
|
||||
logger.info(`Loaded extensions: ${loadedExtensions.map((ext) => ext.name).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +175,38 @@ class ExtensionManager {
|
||||
});
|
||||
}
|
||||
|
||||
public getExtensionsList(type?: ExtensionType): string[] {
|
||||
public getExtensionsList(type?: ExtensionType) {
|
||||
if (type === undefined) {
|
||||
return this.extensions.map((extension) => extension.name);
|
||||
return this.extensions.map(mapInfo);
|
||||
} else {
|
||||
return this.extensions.filter((extension) => extension.type === type).map((extension) => extension.name);
|
||||
return this.extensions.map(mapInfo).filter((extension) => extension.type === type);
|
||||
}
|
||||
|
||||
function mapInfo(extension: Extension): ExtensionInfo {
|
||||
const extensionInfo = {
|
||||
name: extension.name,
|
||||
type: extension.type,
|
||||
local: extension.local,
|
||||
host: extension.host,
|
||||
version: extension.version,
|
||||
};
|
||||
|
||||
if (extension.type === 'bundle') {
|
||||
return {
|
||||
...extensionInfo,
|
||||
entries: extension.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
type: entry.type,
|
||||
})),
|
||||
};
|
||||
} else {
|
||||
return extensionInfo as ExtensionInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getExtension(name: string): Extension | undefined {
|
||||
return this.extensions.find((extension) => extension.name === name);
|
||||
}
|
||||
|
||||
public getAppExtensions(): string | null {
|
||||
@@ -205,7 +231,7 @@ class ExtensionManager {
|
||||
|
||||
private async load(): Promise<void> {
|
||||
try {
|
||||
await ensureExtensionDirs(env.EXTENSIONS_PATH, env.SERVE_APP ? EXTENSION_TYPES : API_OR_HYBRID_EXTENSION_TYPES);
|
||||
await ensureExtensionDirs(env.EXTENSIONS_PATH, NESTED_EXTENSION_TYPES);
|
||||
|
||||
this.extensions = await this.getExtensions();
|
||||
} catch (err: any) {
|
||||
@@ -241,12 +267,14 @@ class ExtensionManager {
|
||||
if (!this.watcher) {
|
||||
logger.info('Watching extensions for changes...');
|
||||
|
||||
const localExtensionPaths = (env.SERVE_APP ? EXTENSION_TYPES : API_OR_HYBRID_EXTENSION_TYPES).flatMap((type) => {
|
||||
const localExtensionPaths = NESTED_EXTENSION_TYPES.flatMap((type) => {
|
||||
const typeDir = path.posix.join(pathToRelativeUrl(env.EXTENSIONS_PATH), pluralize(type));
|
||||
|
||||
return isIn(type, HYBRID_EXTENSION_TYPES)
|
||||
? [path.posix.join(typeDir, '*', 'app.js'), path.posix.join(typeDir, '*', 'api.js')]
|
||||
: path.posix.join(typeDir, '*', 'index.js');
|
||||
if (isIn(type, HYBRID_EXTENSION_TYPES)) {
|
||||
return [path.posix.join(typeDir, '*', 'app.js'), path.posix.join(typeDir, '*', 'api.js')];
|
||||
} else {
|
||||
return path.posix.join(typeDir, '*', 'index.js');
|
||||
}
|
||||
});
|
||||
|
||||
this.watcher = chokidar.watch([path.resolve('package.json'), ...localExtensionPaths], {
|
||||
@@ -272,9 +300,7 @@ class ExtensionManager {
|
||||
extensions
|
||||
.filter((extension) => !extension.local)
|
||||
.flatMap((extension) =>
|
||||
extension.type === 'pack'
|
||||
? path.resolve(extension.path, 'package.json')
|
||||
: isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
|
||||
isTypeIn(extension, HYBRID_EXTENSION_TYPES) || extension.type === 'bundle'
|
||||
? [
|
||||
path.resolve(extension.path, extension.entrypoint.app),
|
||||
path.resolve(extension.path, extension.entrypoint.api),
|
||||
@@ -291,16 +317,13 @@ class ExtensionManager {
|
||||
}
|
||||
|
||||
private async getExtensions(): Promise<Extension[]> {
|
||||
const packageExtensions = await getPackageExtensions(
|
||||
'.',
|
||||
env.SERVE_APP ? EXTENSION_PACKAGE_TYPES : API_OR_HYBRID_EXTENSION_PACKAGE_TYPES
|
||||
);
|
||||
const localExtensions = await getLocalExtensions(
|
||||
env.EXTENSIONS_PATH,
|
||||
env.SERVE_APP ? EXTENSION_TYPES : API_OR_HYBRID_EXTENSION_TYPES
|
||||
);
|
||||
const packageExtensions = await getPackageExtensions('.');
|
||||
const localPackageExtensions = await resolvePackageExtensions(env.EXTENSIONS_PATH);
|
||||
const localExtensions = await getLocalExtensions(env.EXTENSIONS_PATH);
|
||||
|
||||
return [...packageExtensions, ...localExtensions];
|
||||
return [...packageExtensions, ...localPackageExtensions, ...localExtensions].filter(
|
||||
(extension) => env.SERVE_APP || APP_EXTENSION_TYPES.includes(extension.type as any) === false
|
||||
);
|
||||
}
|
||||
|
||||
private async generateExtensionBundle(): Promise<string | null> {
|
||||
|
||||
@@ -27,7 +27,7 @@ const onDehydrateCallbacks: (() => Promise<void>)[] = [];
|
||||
export async function loadExtensions(): Promise<void> {
|
||||
try {
|
||||
customExtensions = import.meta.env.DEV
|
||||
? await import('@directus-extensions')
|
||||
? await import(/* @vite-ignore */ '@directus-extensions')
|
||||
: await import(/* @vite-ignore */ `${getRootPath()}extensions/sources/index.js`);
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
APP_OR_HYBRID_EXTENSION_PACKAGE_TYPES,
|
||||
APP_OR_HYBRID_EXTENSION_TYPES,
|
||||
APP_SHARED_DEPS,
|
||||
NESTED_EXTENSION_TYPES,
|
||||
} from '@directus/shared/constants';
|
||||
import {
|
||||
ensureExtensionDirs,
|
||||
@@ -220,7 +221,7 @@ function directusExtensions() {
|
||||
];
|
||||
|
||||
async function loadExtensions() {
|
||||
await ensureExtensionDirs(EXTENSIONS_PATH, APP_OR_HYBRID_EXTENSION_TYPES);
|
||||
await ensureExtensionDirs(EXTENSIONS_PATH, NESTED_EXTENSION_TYPES);
|
||||
const packageExtensions = await getPackageExtensions(API_PATH, APP_OR_HYBRID_EXTENSION_PACKAGE_TYPES);
|
||||
const localExtensions = await getLocalExtensions(EXTENSIONS_PATH, APP_OR_HYBRID_EXTENSION_TYPES);
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import fse from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import { log } from '../utils/logger';
|
||||
import {
|
||||
ExtensionManifestRaw,
|
||||
ExtensionManifest,
|
||||
ExtensionOptions,
|
||||
ExtensionOptionsBundleEntry,
|
||||
ExtensionType,
|
||||
NestedExtensionType,
|
||||
} from '@directus/shared/types';
|
||||
import { isIn, isTypeIn, validateExtensionManifest } from '@directus/shared/utils';
|
||||
import { isIn, isTypeIn } from '@directus/shared/utils';
|
||||
import { pathToRelativeUrl } from '@directus/shared/utils/node';
|
||||
import {
|
||||
EXTENSION_LANGUAGES,
|
||||
@@ -36,28 +36,25 @@ export default async function add(): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const extensionManifestFile = await fse.readFile(packagePath, 'utf8');
|
||||
const extensionManifest: ExtensionManifestRaw = JSON.parse(extensionManifestFile);
|
||||
let extensionManifest: ExtensionManifest;
|
||||
let indent: string | null = null;
|
||||
|
||||
const indent = detectJsonIndent(extensionManifestFile);
|
||||
|
||||
if (!validateExtensionManifest(extensionManifest)) {
|
||||
try {
|
||||
const extensionManifestFile = await fse.readFile(packagePath, 'utf8');
|
||||
extensionManifest = ExtensionManifest.parse(JSON.parse(extensionManifestFile));
|
||||
indent = detectJsonIndent(extensionManifestFile);
|
||||
} catch (e) {
|
||||
log(`Current directory is not a valid Directus extension.`, 'error');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const extensionOptions = extensionManifest[EXTENSION_PKG_KEY];
|
||||
|
||||
if (extensionOptions.type === 'pack') {
|
||||
log(`Adding entries to extensions with type ${chalk.bold('pack')} is not currently supported.`, 'error');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sourceExists = await fse.pathExists(path.resolve('src'));
|
||||
|
||||
if (extensionOptions.type === 'bundle') {
|
||||
const { type, name, language, alternativeSource } = await inquirer.prompt<{
|
||||
type: ExtensionType;
|
||||
type: NestedExtensionType;
|
||||
name: string;
|
||||
language: Language;
|
||||
alternativeSource?: string;
|
||||
@@ -149,7 +146,7 @@ export default async function add(): Promise<void> {
|
||||
const oldName = extensionManifest.name.match(EXTENSION_NAME_REGEX)?.[1] ?? extensionManifest.name;
|
||||
|
||||
const { type, name, language, convertName, extensionName, alternativeSource } = await inquirer.prompt<{
|
||||
type: ExtensionType;
|
||||
type: NestedExtensionType;
|
||||
name: string;
|
||||
language: Language;
|
||||
convertName: string;
|
||||
|
||||
@@ -2,17 +2,18 @@ import {
|
||||
API_SHARED_DEPS,
|
||||
APP_EXTENSION_TYPES,
|
||||
APP_SHARED_DEPS,
|
||||
EXTENSION_PACKAGE_TYPES,
|
||||
EXTENSION_PKG_KEY,
|
||||
EXTENSION_TYPES,
|
||||
HYBRID_EXTENSION_TYPES,
|
||||
} from '@directus/shared/constants';
|
||||
import {
|
||||
ApiExtensionType,
|
||||
AppExtensionType,
|
||||
ExtensionManifestRaw,
|
||||
ExtensionManifest,
|
||||
ExtensionOptionsBundleEntries,
|
||||
ExtensionOptionsBundleEntry,
|
||||
} from '@directus/shared/types';
|
||||
import { isIn, isTypeIn, validateExtensionManifest } from '@directus/shared/utils';
|
||||
import { isIn, isTypeIn } from '@directus/shared/utils';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
@@ -40,7 +41,7 @@ import { clear, log } from '../utils/logger';
|
||||
import tryParseJson from '../utils/try-parse-json';
|
||||
import generateBundleEntrypoint from './helpers/generate-bundle-entrypoint';
|
||||
import loadConfig from './helpers/load-config';
|
||||
import { validateBundleEntriesOption, validateSplitEntrypointOption } from './helpers/validate-cli-options';
|
||||
import { validateSplitEntrypointOption } from './helpers/validate-cli-options';
|
||||
|
||||
type BuildOptions = {
|
||||
type?: string;
|
||||
@@ -64,20 +65,17 @@ export default async function build(options: BuildOptions): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const extensionManifest: ExtensionManifestRaw = await fse.readJSON(packagePath);
|
||||
let extensionManifest: ExtensionManifest;
|
||||
|
||||
if (!validateExtensionManifest(extensionManifest)) {
|
||||
try {
|
||||
extensionManifest = ExtensionManifest.parse(await fse.readJSON(packagePath));
|
||||
} catch (err) {
|
||||
log(`Current directory is not a valid Directus extension.`, 'error');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const extensionOptions = extensionManifest[EXTENSION_PKG_KEY];
|
||||
|
||||
if (extensionOptions.type === 'pack') {
|
||||
log(`Building extension type ${chalk.bold('pack')} is not currently supported.`, 'error');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (extensionOptions.type === 'bundle') {
|
||||
await buildBundleExtension({
|
||||
entries: extensionOptions.entries,
|
||||
@@ -117,21 +115,16 @@ export default async function build(options: BuildOptions): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isIn(type, EXTENSION_PACKAGE_TYPES)) {
|
||||
if (!isIn(type, EXTENSION_TYPES)) {
|
||||
log(
|
||||
`Extension type ${chalk.bold(type)} is not supported. Available extension types: ${EXTENSION_PACKAGE_TYPES.map(
|
||||
(t) => chalk.bold.magenta(t)
|
||||
`Extension type ${chalk.bold(type)} is not supported. Available extension types: ${EXTENSION_TYPES.map((t) =>
|
||||
chalk.bold.magenta(t)
|
||||
).join(', ')}.`,
|
||||
'error'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (type === 'pack') {
|
||||
log(`Building extension type ${chalk.bold('pack')} is not currently supported.`, 'error');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
log(`Extension entrypoint has to be specified using the ${chalk.blue('[-i, --input <file>]')} option.`, 'error');
|
||||
process.exit(1);
|
||||
@@ -145,10 +138,10 @@ export default async function build(options: BuildOptions): Promise<void> {
|
||||
}
|
||||
|
||||
if (type === 'bundle') {
|
||||
const entries = tryParseJson(input);
|
||||
const entries = ExtensionOptionsBundleEntries.safeParse(tryParseJson(input));
|
||||
const splitOutput = tryParseJson(output);
|
||||
|
||||
if (!validateBundleEntriesOption(entries)) {
|
||||
if (entries.success === false) {
|
||||
log(
|
||||
`Input option needs to be of the format ${chalk.blue(
|
||||
`[-i '[{"type":"<extension-type>","name":"<extension-name>","source":<entrypoint>}]']`
|
||||
@@ -168,7 +161,7 @@ export default async function build(options: BuildOptions): Promise<void> {
|
||||
}
|
||||
|
||||
await buildBundleExtension({
|
||||
entries,
|
||||
entries: entries.data,
|
||||
outputApp: splitOutput.app,
|
||||
outputApi: splitOutput.api,
|
||||
watch,
|
||||
|
||||
@@ -8,11 +8,18 @@ import {
|
||||
EXTENSION_LANGUAGES,
|
||||
HYBRID_EXTENSION_TYPES,
|
||||
EXTENSION_NAME_REGEX,
|
||||
EXTENSION_PACKAGE_TYPES,
|
||||
PACKAGE_EXTENSION_TYPES,
|
||||
EXTENSION_TYPES,
|
||||
BUNDLE_EXTENSION_TYPES,
|
||||
} from '@directus/shared/constants';
|
||||
import { isIn } from '@directus/shared/utils';
|
||||
import { ExtensionOptions, ExtensionPackageType, ExtensionType, PackageExtensionType } from '@directus/shared/types';
|
||||
import {
|
||||
ApiExtensionType,
|
||||
AppExtensionType,
|
||||
BundleExtensionType,
|
||||
ExtensionOptions,
|
||||
ExtensionType,
|
||||
HybridExtensionType,
|
||||
} from '@directus/shared/types';
|
||||
import { log } from '../utils/logger';
|
||||
import { isLanguage, languageToShort } from '../utils/languages';
|
||||
import getSdkVersion from '../utils/get-sdk-version';
|
||||
@@ -26,10 +33,10 @@ export default async function create(type: string, name: string, options: Create
|
||||
const targetDir = name.substring(name.lastIndexOf('/') + 1);
|
||||
const targetPath = path.resolve(targetDir);
|
||||
|
||||
if (!isIn(type, EXTENSION_PACKAGE_TYPES)) {
|
||||
if (!isIn(type, EXTENSION_TYPES)) {
|
||||
log(
|
||||
`Extension type ${chalk.bold(type)} is not supported. Available extension types: ${EXTENSION_PACKAGE_TYPES.map(
|
||||
(t) => chalk.bold.magenta(t)
|
||||
`Extension type ${chalk.bold(type)} is not supported. Available extension types: ${EXTENSION_TYPES.map((t) =>
|
||||
chalk.bold.magenta(t)
|
||||
).join(', ')}.`,
|
||||
'error'
|
||||
);
|
||||
@@ -57,7 +64,7 @@ export default async function create(type: string, name: string, options: Create
|
||||
}
|
||||
}
|
||||
|
||||
if (isIn(type, PACKAGE_EXTENSION_TYPES)) {
|
||||
if (isIn(type, BUNDLE_EXTENSION_TYPES)) {
|
||||
await createPackageExtension({ type, name, targetDir, targetPath });
|
||||
} else {
|
||||
const language = options.language ?? 'javascript';
|
||||
@@ -72,7 +79,7 @@ async function createPackageExtension({
|
||||
targetDir,
|
||||
targetPath,
|
||||
}: {
|
||||
type: PackageExtensionType;
|
||||
type: BundleExtensionType;
|
||||
name: string;
|
||||
targetDir: string;
|
||||
targetPath: string;
|
||||
@@ -83,8 +90,7 @@ async function createPackageExtension({
|
||||
await copyTemplate(type, targetPath);
|
||||
|
||||
const host = `^${getSdkVersion()}`;
|
||||
const options: ExtensionOptions =
|
||||
type === 'bundle' ? { type, path: { app: 'dist/app.js', api: 'dist/api.js' }, entries: [], host } : { type, host };
|
||||
const options = { type, path: { app: 'dist/app.js', api: 'dist/api.js' }, entries: [], host };
|
||||
const packageManifest = getPackageManifest(name, options, await getExtensionDevDeps(type));
|
||||
|
||||
await fse.writeJSON(path.join(targetPath, 'package.json'), packageManifest, { spaces: '\t' });
|
||||
@@ -105,7 +111,7 @@ async function createLocalExtension({
|
||||
targetPath,
|
||||
language,
|
||||
}: {
|
||||
type: ExtensionType;
|
||||
type: AppExtensionType | ApiExtensionType | HybridExtensionType;
|
||||
name: string;
|
||||
targetDir: string;
|
||||
targetPath: string;
|
||||
@@ -154,20 +160,29 @@ async function createLocalExtension({
|
||||
}
|
||||
|
||||
function getPackageManifest(name: string, options: ExtensionOptions, deps: Record<string, string>) {
|
||||
return {
|
||||
const packageManifest: Record<string, any> = {
|
||||
name: EXTENSION_NAME_REGEX.test(name) ? name : `directus-extension-${name}`,
|
||||
description: 'Please enter a description for your extension',
|
||||
icon: 'extension',
|
||||
version: '1.0.0',
|
||||
keywords: ['directus', 'directus-extension', `directus-custom-${options.type}`],
|
||||
[EXTENSION_PKG_KEY]: options,
|
||||
scripts: {
|
||||
build: 'directus-extension build',
|
||||
dev: 'directus-extension build -w --no-minify',
|
||||
link: 'directus-extension link',
|
||||
},
|
||||
devDependencies: deps,
|
||||
};
|
||||
|
||||
if (options.type === 'bundle') {
|
||||
packageManifest.scripts['add'] = 'directus-extension add';
|
||||
}
|
||||
|
||||
return packageManifest;
|
||||
}
|
||||
|
||||
function getDoneMessage(type: ExtensionPackageType, targetDir: string, targetPath: string, packageManager: string) {
|
||||
function getDoneMessage(type: ExtensionType, targetDir: string, targetPath: string, packageManager: string) {
|
||||
return `
|
||||
Your ${type} extension has been created at ${chalk.green(targetPath)}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import getTemplatePath from '../../utils/get-template-path';
|
||||
import { ExtensionPackageType } from '@directus/shared/types';
|
||||
import { ExtensionType } from '@directus/shared/types';
|
||||
import { Language } from '../../types';
|
||||
|
||||
type TemplateFile = { type: 'config' | 'source'; path: string };
|
||||
@@ -49,7 +49,7 @@ async function getTypeTemplateFiles(templateTypePath: string, language?: Languag
|
||||
return [...commonTemplateFiles, ...(languageTemplateFiles ? languageTemplateFiles : [])];
|
||||
}
|
||||
|
||||
async function getTemplateFiles(type: ExtensionPackageType, language?: Language): Promise<TemplateFile[]> {
|
||||
async function getTemplateFiles(type: ExtensionType, language?: Language): Promise<TemplateFile[]> {
|
||||
const templatePath = getTemplatePath();
|
||||
|
||||
const [commonTemplateFiles, typeTemplateFiles] = await Promise.all([
|
||||
@@ -61,7 +61,7 @@ async function getTemplateFiles(type: ExtensionPackageType, language?: Language)
|
||||
}
|
||||
|
||||
export default async function copyTemplate(
|
||||
type: ExtensionPackageType,
|
||||
type: ExtensionType,
|
||||
extensionPath: string,
|
||||
sourcePath?: string,
|
||||
language?: Language
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import path from 'path';
|
||||
import {
|
||||
API_OR_HYBRID_EXTENSION_TYPES,
|
||||
APP_OR_HYBRID_EXTENSION_TYPES,
|
||||
HYBRID_EXTENSION_TYPES,
|
||||
} from '@directus/shared/constants';
|
||||
import { API_EXTENSION_TYPES, APP_EXTENSION_TYPES, HYBRID_EXTENSION_TYPES } from '@directus/shared/constants';
|
||||
import { ExtensionOptionsBundleEntry } from '@directus/shared/types';
|
||||
import { isIn, isTypeIn, pluralize } from '@directus/shared/utils';
|
||||
import { pathToRelativeUrl } from '@directus/shared/utils/node';
|
||||
|
||||
export default function generateBundleEntrypoint(mode: 'app' | 'api', entries: ExtensionOptionsBundleEntry[]): string {
|
||||
const types = mode === 'app' ? APP_OR_HYBRID_EXTENSION_TYPES : API_OR_HYBRID_EXTENSION_TYPES;
|
||||
const types = [...(mode === 'app' ? APP_EXTENSION_TYPES : API_EXTENSION_TYPES), ...HYBRID_EXTENSION_TYPES];
|
||||
|
||||
const entriesForTypes = entries.filter((entry) => isIn(entry.type, types));
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { API_OR_HYBRID_EXTENSION_TYPES, APP_OR_HYBRID_EXTENSION_TYPES } from '@directus/shared/constants';
|
||||
import { ExtensionPackageType } from '@directus/shared/types';
|
||||
import { APP_EXTENSION_TYPES, API_EXTENSION_TYPES, HYBRID_EXTENSION_TYPES } from '@directus/shared/constants';
|
||||
import { ExtensionType } from '@directus/shared/types';
|
||||
import { isIn } from '@directus/shared/utils';
|
||||
import { Language } from '../../types';
|
||||
import getPackageVersion from '../../utils/get-package-version';
|
||||
import getSdkVersion from '../../utils/get-sdk-version';
|
||||
|
||||
export default async function getExtensionDevDeps(
|
||||
type: ExtensionPackageType | ExtensionPackageType[],
|
||||
type: ExtensionType | ExtensionType[],
|
||||
language: Language | Language[] = []
|
||||
): Promise<Record<string, string>> {
|
||||
const types = Array.isArray(type) ? type : [type];
|
||||
@@ -17,14 +17,14 @@ export default async function getExtensionDevDeps(
|
||||
};
|
||||
|
||||
if (languages.includes('typescript')) {
|
||||
if (types.some((type) => isIn(type, API_OR_HYBRID_EXTENSION_TYPES))) {
|
||||
if (types.some((type) => isIn(type, [...API_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES]))) {
|
||||
deps['@types/node'] = `^${await getPackageVersion('@types/node')}`;
|
||||
}
|
||||
|
||||
deps['typescript'] = `^${await getPackageVersion('typescript')}`;
|
||||
}
|
||||
|
||||
if (types.some((type) => isIn(type, APP_OR_HYBRID_EXTENSION_TYPES))) {
|
||||
if (types.some((type) => isIn(type, [...APP_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES]))) {
|
||||
deps['vue'] = `^${await getPackageVersion('vue')}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ExtensionOptionsBundleEntry, JsonValue, SplitEntrypoint } from '@directus/shared/types';
|
||||
import { validateExtensionOptionsBundleEntry } from '@directus/shared/utils';
|
||||
import { JsonValue, SplitEntrypoint } from '@directus/shared/types';
|
||||
|
||||
function validateNonPrimitive(value: JsonValue | undefined): value is JsonValue[] | { [key: string]: JsonValue } {
|
||||
if (
|
||||
@@ -26,23 +25,3 @@ export function validateSplitEntrypointOption(option: JsonValue | undefined): op
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateBundleEntriesOption(option: JsonValue | undefined): option is ExtensionOptionsBundleEntry[] {
|
||||
if (!validateNonPrimitive(option) || !Array.isArray(option)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!option.every((entry) => {
|
||||
if (!validateNonPrimitive(entry) || Array.isArray(entry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return validateExtensionOptionsBundleEntry(entry);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
61
packages/extensions-sdk/src/cli/commands/link.ts
Normal file
61
packages/extensions-sdk/src/cli/commands/link.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { log } from '../utils/logger';
|
||||
import { ExtensionManifest } from '@directus/shared/types';
|
||||
|
||||
export default async function link(extensionsPath: string): Promise<void> {
|
||||
const extensionPath = process.cwd();
|
||||
|
||||
const absoluteExtensionsPath = path.resolve(extensionsPath);
|
||||
|
||||
if (!fs.existsSync(absoluteExtensionsPath)) {
|
||||
log(`Extensions folder does not exist at ${absoluteExtensionsPath}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const packagePath = path.resolve('package.json');
|
||||
|
||||
if (!(await fs.pathExists(packagePath))) {
|
||||
log(`Current directory is not a valid package.`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let manifestFile: Record<string, any>;
|
||||
|
||||
try {
|
||||
manifestFile = await fs.readJSON(packagePath);
|
||||
} catch (err) {
|
||||
log(`Current directory is not a valid Directus extension.`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionManifest = ExtensionManifest.parse(manifestFile);
|
||||
|
||||
const extensionName = extensionManifest.name;
|
||||
|
||||
if (!extensionName) {
|
||||
log(`Extension name not found in package.json`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const type = extensionManifest['directus:extension']?.type;
|
||||
|
||||
if (!type) {
|
||||
log(`Extension type not found in package.json`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionTarget = path.join(absoluteExtensionsPath, extensionName);
|
||||
|
||||
try {
|
||||
fs.ensureSymlinkSync(extensionPath, extensionTarget);
|
||||
} catch (error: any) {
|
||||
log(error.message, 'error');
|
||||
log(`Try running this command with administrator privileges`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Linked ${extensionName} to ${extensionTarget}`);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
||||
import create from './commands/create';
|
||||
import add from './commands/add';
|
||||
import build from './commands/build';
|
||||
import link from './commands/link';
|
||||
|
||||
const pkg = require('../../../package.json');
|
||||
|
||||
@@ -32,4 +33,10 @@ program
|
||||
.option('--sourcemap', 'include source maps in output')
|
||||
.action(build);
|
||||
|
||||
program
|
||||
.command('link')
|
||||
.description('Creates a symlink to the extension in the Directus extensions folder')
|
||||
.argument('<path>', 'path to the extension folder of directus')
|
||||
.action(link);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
"pino": "8.8.0",
|
||||
"vue": "3.2.45",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6"
|
||||
"vue-router": "4.1.6",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "4.17.15",
|
||||
|
||||
@@ -4,20 +4,22 @@ export const API_SHARED_DEPS = ['directus'];
|
||||
export const APP_EXTENSION_TYPES = ['interface', 'display', 'layout', 'module', 'panel'] as const;
|
||||
export const API_EXTENSION_TYPES = ['hook', 'endpoint'] as const;
|
||||
export const HYBRID_EXTENSION_TYPES = ['operation'] as const;
|
||||
export const EXTENSION_TYPES = [...APP_EXTENSION_TYPES, ...API_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES] as const;
|
||||
|
||||
export const PACKAGE_EXTENSION_TYPES = ['pack', 'bundle'] as const;
|
||||
export const EXTENSION_PACKAGE_TYPES = [...EXTENSION_TYPES, ...PACKAGE_EXTENSION_TYPES] as const;
|
||||
|
||||
export const BUNDLE_EXTENSION_TYPES = ['bundle'] as const;
|
||||
export const EXTENSION_TYPES = [
|
||||
...APP_EXTENSION_TYPES,
|
||||
...API_EXTENSION_TYPES,
|
||||
...HYBRID_EXTENSION_TYPES,
|
||||
...BUNDLE_EXTENSION_TYPES,
|
||||
] as const;
|
||||
export const NESTED_EXTENSION_TYPES = [
|
||||
...APP_EXTENSION_TYPES,
|
||||
...API_EXTENSION_TYPES,
|
||||
...HYBRID_EXTENSION_TYPES,
|
||||
] as const;
|
||||
export const APP_OR_HYBRID_EXTENSION_TYPES = [...APP_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES] as const;
|
||||
export const API_OR_HYBRID_EXTENSION_TYPES = [...API_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES] as const;
|
||||
export const APP_OR_HYBRID_EXTENSION_PACKAGE_TYPES = [
|
||||
...APP_OR_HYBRID_EXTENSION_TYPES,
|
||||
...PACKAGE_EXTENSION_TYPES,
|
||||
] as const;
|
||||
export const API_OR_HYBRID_EXTENSION_PACKAGE_TYPES = [
|
||||
...API_OR_HYBRID_EXTENSION_TYPES,
|
||||
...PACKAGE_EXTENSION_TYPES,
|
||||
...BUNDLE_EXTENSION_TYPES,
|
||||
] as const;
|
||||
|
||||
export const EXTENSION_LANGUAGES = ['javascript', 'typescript'] as const;
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Logger } from 'pino';
|
||||
import {
|
||||
API_EXTENSION_TYPES,
|
||||
APP_EXTENSION_TYPES,
|
||||
EXTENSION_PACKAGE_TYPES,
|
||||
BUNDLE_EXTENSION_TYPES,
|
||||
EXTENSION_PKG_KEY,
|
||||
EXTENSION_TYPES,
|
||||
HYBRID_EXTENSION_TYPES,
|
||||
LOCAL_TYPES,
|
||||
PACKAGE_EXTENSION_TYPES,
|
||||
NESTED_EXTENSION_TYPES,
|
||||
} from '../constants';
|
||||
import { Accountability } from './accountability';
|
||||
import { InterfaceConfig } from './interfaces';
|
||||
@@ -22,137 +22,107 @@ import { Relation } from './relations';
|
||||
import { Collection } from './collection';
|
||||
import { SchemaOverview } from './schema';
|
||||
import { OperationAppConfig } from './operations';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type AppExtensionType = typeof APP_EXTENSION_TYPES[number];
|
||||
export type ApiExtensionType = typeof API_EXTENSION_TYPES[number];
|
||||
export type HybridExtensionType = typeof HYBRID_EXTENSION_TYPES[number];
|
||||
export type BundleExtensionType = typeof BUNDLE_EXTENSION_TYPES[number];
|
||||
export type ExtensionType = typeof EXTENSION_TYPES[number];
|
||||
export type NestedExtensionType = typeof NESTED_EXTENSION_TYPES[number];
|
||||
|
||||
export type PackageExtensionType = typeof PACKAGE_EXTENSION_TYPES[number];
|
||||
export type ExtensionPackageType = typeof EXTENSION_PACKAGE_TYPES[number];
|
||||
const SplitEntrypoint = z.object({
|
||||
app: z.string(),
|
||||
api: z.string(),
|
||||
});
|
||||
|
||||
export type SplitEntrypoint = { app: string; api: string };
|
||||
export type SplitEntrypoint = z.infer<typeof SplitEntrypoint>;
|
||||
|
||||
type ExtensionBase = {
|
||||
path: string;
|
||||
name: string;
|
||||
version?: string;
|
||||
host?: string;
|
||||
local: boolean;
|
||||
};
|
||||
|
||||
type AppExtensionBase = {
|
||||
export type AppExtension = ExtensionBase & {
|
||||
type: AppExtensionType;
|
||||
entrypoint: string;
|
||||
};
|
||||
|
||||
type ApiExtensionBase = {
|
||||
export type ApiExtension = ExtensionBase & {
|
||||
type: ApiExtensionType;
|
||||
entrypoint: string;
|
||||
};
|
||||
|
||||
type HybridExtensionBase = {
|
||||
export type HybridExtension = ExtensionBase & {
|
||||
type: HybridExtensionType;
|
||||
entrypoint: SplitEntrypoint;
|
||||
};
|
||||
|
||||
type PackExtensionBase = {
|
||||
type: 'pack';
|
||||
children: string[];
|
||||
};
|
||||
|
||||
type BundleExtensionBase = {
|
||||
export type BundleExtension = ExtensionBase & {
|
||||
type: 'bundle';
|
||||
entrypoint: SplitEntrypoint;
|
||||
entries: { type: ExtensionType; name: string }[];
|
||||
entries: { type: NestedExtensionType; name: string }[];
|
||||
};
|
||||
|
||||
type PackageExtensionBase = PackExtensionBase | BundleExtensionBase;
|
||||
export type Extension = AppExtension | ApiExtension | HybridExtension | BundleExtension;
|
||||
|
||||
type ExtensionLocalBase = ExtensionBase & {
|
||||
local: true;
|
||||
};
|
||||
export const ExtensionOptionsBundleEntry = z.union([
|
||||
z.object({
|
||||
type: z.union([z.enum(APP_EXTENSION_TYPES), z.enum(API_EXTENSION_TYPES)]),
|
||||
name: z.string(),
|
||||
source: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.enum(HYBRID_EXTENSION_TYPES),
|
||||
name: z.string(),
|
||||
source: SplitEntrypoint,
|
||||
}),
|
||||
]);
|
||||
|
||||
type ExtensionPackageBase = ExtensionBase & {
|
||||
version: string;
|
||||
host: string;
|
||||
local: false;
|
||||
};
|
||||
const ExtensionOptionsBase = z.object({
|
||||
host: z.string(),
|
||||
hidden: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type ExtensionLocal = ExtensionLocalBase & (AppExtensionBase | ApiExtensionBase | HybridExtensionBase);
|
||||
export type ExtensionPackage = ExtensionPackageBase &
|
||||
(AppExtensionBase | ApiExtensionBase | HybridExtensionBase | PackageExtensionBase);
|
||||
const ExtensionOptionsAppOrApi = z.object({
|
||||
type: z.union([z.enum(APP_EXTENSION_TYPES), z.enum(API_EXTENSION_TYPES)]),
|
||||
path: z.string(),
|
||||
source: z.string(),
|
||||
});
|
||||
|
||||
export type AppExtension = AppExtensionBase & (ExtensionLocalBase | ExtensionPackageBase);
|
||||
export type ApiExtension = ApiExtensionBase & (ExtensionLocalBase | ExtensionPackageBase);
|
||||
export type HybridExtension = HybridExtensionBase & (ExtensionLocalBase | ExtensionPackageBase);
|
||||
const ExtensionOptionsHybrid = z.object({
|
||||
type: z.enum(HYBRID_EXTENSION_TYPES),
|
||||
path: SplitEntrypoint,
|
||||
source: SplitEntrypoint,
|
||||
});
|
||||
|
||||
export type PackExtension = PackExtensionBase & ExtensionPackageBase;
|
||||
export type BundleExtension = BundleExtensionBase & ExtensionPackageBase;
|
||||
const ExtensionOptionsBundle = z.object({
|
||||
type: z.literal('bundle'),
|
||||
path: SplitEntrypoint,
|
||||
entries: z.array(ExtensionOptionsBundleEntry),
|
||||
});
|
||||
|
||||
export type Extension = ExtensionLocal | ExtensionPackage;
|
||||
const ExtensionOptions = ExtensionOptionsBase.and(
|
||||
z.union([ExtensionOptionsAppOrApi, ExtensionOptionsHybrid, ExtensionOptionsBundle])
|
||||
);
|
||||
|
||||
export type ExtensionOptionsBundleEntryRaw = {
|
||||
type?: string;
|
||||
name?: string;
|
||||
source?: string | Partial<SplitEntrypoint>;
|
||||
};
|
||||
export type ExtensionOptions = z.infer<typeof ExtensionOptions>;
|
||||
export type ExtensionOptionsBundleEntry = z.infer<typeof ExtensionOptionsBundleEntry>;
|
||||
|
||||
export type ExtensionManifestRaw = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
dependencies?: Record<string, string>;
|
||||
export const ExtensionOptionsBundleEntries = z.array(ExtensionOptionsBundleEntry);
|
||||
export type ExtensionOptionsBundleEntries = z.infer<typeof ExtensionOptionsBundleEntries>;
|
||||
|
||||
[EXTENSION_PKG_KEY]?: {
|
||||
type?: string;
|
||||
path?: string | Partial<SplitEntrypoint>;
|
||||
source?: string | Partial<SplitEntrypoint>;
|
||||
entries?: ExtensionOptionsBundleEntryRaw[];
|
||||
host?: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
};
|
||||
export const ExtensionManifest = z.object({
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
[EXTENSION_PKG_KEY]: ExtensionOptions,
|
||||
});
|
||||
|
||||
export type ExtensionOptionsBundleEntry =
|
||||
| { type: AppExtensionType | ApiExtensionType; name: string; source: string }
|
||||
| { type: HybridExtensionType; name: string; source: SplitEntrypoint };
|
||||
|
||||
type ExtensionOptionsBase = {
|
||||
host: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
type ExtensionOptionsAppOrApi = {
|
||||
type: AppExtensionType | ApiExtensionType;
|
||||
path: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
type ExtensionOptionsHybrid = {
|
||||
type: HybridExtensionType;
|
||||
path: SplitEntrypoint;
|
||||
source: SplitEntrypoint;
|
||||
};
|
||||
|
||||
type ExtensionOptionsPack = {
|
||||
type: 'pack';
|
||||
};
|
||||
|
||||
type ExtensionOptionsBundle = {
|
||||
type: 'bundle';
|
||||
path: SplitEntrypoint;
|
||||
entries: ExtensionOptionsBundleEntry[];
|
||||
};
|
||||
|
||||
type ExtensionOptionsPackage = ExtensionOptionsPack | ExtensionOptionsBundle;
|
||||
|
||||
export type ExtensionOptions = ExtensionOptionsBase &
|
||||
(ExtensionOptionsAppOrApi | ExtensionOptionsHybrid | ExtensionOptionsPackage);
|
||||
|
||||
export type ExtensionManifest = {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies?: Record<string, string>;
|
||||
|
||||
[EXTENSION_PKG_KEY]: ExtensionOptions;
|
||||
};
|
||||
export type ExtensionManifest = z.infer<typeof ExtensionManifest>;
|
||||
|
||||
export type AppExtensionConfigs = {
|
||||
interfaces: InterfaceConfig[];
|
||||
@@ -198,3 +168,9 @@ export type ExtensionOptionsContext = {
|
||||
autoGenerateJunctionRelation: boolean;
|
||||
saving: boolean;
|
||||
};
|
||||
|
||||
export type ExtensionInfo =
|
||||
| Omit<AppExtension, 'entrypoint' | 'path'>
|
||||
| Omit<ApiExtension, 'entrypoint' | 'path'>
|
||||
| Omit<HybridExtension, 'entrypoint' | 'path'>
|
||||
| Omit<BundleExtension, 'entrypoint' | 'path'>;
|
||||
|
||||
@@ -24,5 +24,4 @@ export * from './parse-filter';
|
||||
export * from './parse-json';
|
||||
export * from './pluralize';
|
||||
export * from './to-array';
|
||||
export * from './validate-extension-manifest';
|
||||
export * from './validate-payload';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, beforeEach, afterEach, it, expect } from 'vitest';
|
||||
|
||||
import { DirResult, dirSync } from 'tmp';
|
||||
import { EXTENSION_TYPES } from '../../constants/extensions';
|
||||
import { NESTED_EXTENSION_TYPES } from '../../constants/extensions';
|
||||
import { ensureExtensionDirs } from './ensure-extension-dirs';
|
||||
|
||||
describe('ensureExtensionDirs', () => {
|
||||
@@ -16,12 +16,12 @@ describe('ensureExtensionDirs', () => {
|
||||
});
|
||||
|
||||
it('returns undefined if the folders exist', async () => {
|
||||
expect(await ensureExtensionDirs(rootDir.name, EXTENSION_TYPES)).toBe(undefined);
|
||||
expect(await ensureExtensionDirs(rootDir.name, NESTED_EXTENSION_TYPES)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('throws an error when a folder can not be opened', () => {
|
||||
expect(async () => {
|
||||
await ensureExtensionDirs('/.', EXTENSION_TYPES);
|
||||
await ensureExtensionDirs('/.', NESTED_EXTENSION_TYPES);
|
||||
}).rejects.toThrow(`Extension folder "/interfaces" couldn't be opened`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { pluralize } from '../pluralize';
|
||||
import { ExtensionType } from '../../types';
|
||||
import { NestedExtensionType } from '../../types';
|
||||
|
||||
export async function ensureExtensionDirs(extensionsPath: string, types: readonly ExtensionType[]): Promise<void> {
|
||||
export async function ensureExtensionDirs(
|
||||
extensionsPath: string,
|
||||
types: readonly NestedExtensionType[]
|
||||
): Promise<void> {
|
||||
for (const extensionType of types) {
|
||||
const dirPath = path.resolve(extensionsPath, pluralize(extensionType));
|
||||
try {
|
||||
|
||||
@@ -7,12 +7,13 @@ describe('generateExtensionsEntrypoint', () => {
|
||||
it('returns an empty extension entrypoint if there is no App, Hybrid or Bundle extension', () => {
|
||||
const mockExtensions: Extension[] = [
|
||||
{
|
||||
path: './extensions/pack',
|
||||
name: 'mock-pack-extension',
|
||||
type: 'pack',
|
||||
path: './extensions/bundle',
|
||||
name: 'mock-bundle0-extension',
|
||||
version: '1.0.0',
|
||||
type: 'bundle',
|
||||
entrypoint: { app: 'app.js', api: 'api.js' },
|
||||
entries: [],
|
||||
host: '^9.0.0',
|
||||
children: [],
|
||||
local: false,
|
||||
},
|
||||
];
|
||||
@@ -73,15 +74,6 @@ describe('generateExtensionsEntrypoint', () => {
|
||||
|
||||
it('returns an extension entrypoint exporting multiple extensions', () => {
|
||||
const mockExtensions: Extension[] = [
|
||||
{
|
||||
path: './extensions/pack',
|
||||
name: 'mock-pack-extension',
|
||||
type: 'pack',
|
||||
version: '1.0.0',
|
||||
host: '^9.0.0',
|
||||
children: [],
|
||||
local: false,
|
||||
},
|
||||
{
|
||||
path: './extensions/display',
|
||||
name: 'mock-display-extension',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import { APP_OR_HYBRID_EXTENSION_TYPES, HYBRID_EXTENSION_TYPES } from '../../constants';
|
||||
import { HYBRID_EXTENSION_TYPES, APP_EXTENSION_TYPES } from '../../constants';
|
||||
import { AppExtension, BundleExtension, Extension, HybridExtension } from '../../types';
|
||||
import { isIn, isTypeIn } from '../array-helpers';
|
||||
import { pluralize } from '../pluralize';
|
||||
@@ -7,15 +7,16 @@ import { pathToRelativeUrl } from './path-to-relative-url';
|
||||
|
||||
export function generateExtensionsEntrypoint(extensions: Extension[]): string {
|
||||
const appOrHybridExtensions = extensions.filter((extension): extension is AppExtension | HybridExtension =>
|
||||
isIn(extension.type, APP_OR_HYBRID_EXTENSION_TYPES)
|
||||
isIn(extension.type, [...APP_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES])
|
||||
);
|
||||
|
||||
const bundleExtensions = extensions.filter(
|
||||
(extension): extension is BundleExtension =>
|
||||
extension.type === 'bundle' && extension.entries.some((entry) => isIn(entry.type, APP_OR_HYBRID_EXTENSION_TYPES))
|
||||
extension.type === 'bundle' &&
|
||||
extension.entries.some((entry) => isIn(entry.type, [...APP_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES]))
|
||||
);
|
||||
|
||||
const appOrHybridExtensionImports = APP_OR_HYBRID_EXTENSION_TYPES.flatMap((type) =>
|
||||
const appOrHybridExtensionImports = [...APP_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES].flatMap((type) =>
|
||||
appOrHybridExtensions
|
||||
.filter((extension) => extension.type === type)
|
||||
.map(
|
||||
@@ -31,12 +32,13 @@ export function generateExtensionsEntrypoint(extensions: Extension[]): string {
|
||||
|
||||
const bundleExtensionImports = bundleExtensions.map(
|
||||
(extension, i) =>
|
||||
`import {${APP_OR_HYBRID_EXTENSION_TYPES.filter((type) => extension.entries.some((entry) => entry.type === type))
|
||||
`import {${[...APP_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES]
|
||||
.filter((type) => extension.entries.some((entry) => entry.type === type))
|
||||
.map((type) => `${pluralize(type)} as ${type}Bundle${i}`)
|
||||
.join(',')}} from './${pathToRelativeUrl(path.resolve(extension.path, extension.entrypoint.app))}';`
|
||||
);
|
||||
|
||||
const extensionExports = APP_OR_HYBRID_EXTENSION_TYPES.map(
|
||||
const extensionExports = [...APP_EXTENSION_TYPES, ...HYBRID_EXTENSION_TYPES].map(
|
||||
(type) =>
|
||||
`export const ${pluralize(type)} = [${appOrHybridExtensions
|
||||
.filter((extension) => extension.type === type)
|
||||
|
||||
@@ -1,102 +1,85 @@
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import {
|
||||
ExtensionLocal,
|
||||
ExtensionManifestRaw,
|
||||
ExtensionPackage,
|
||||
ExtensionPackageType,
|
||||
ExtensionType,
|
||||
} from '../../types';
|
||||
import { ApiExtensionType, AppExtensionType, Extension, ExtensionManifest } from '../../types';
|
||||
import { resolvePackage } from './resolve-package';
|
||||
import { listFolders } from './list-folders';
|
||||
import { EXTENSION_NAME_REGEX, EXTENSION_PKG_KEY, HYBRID_EXTENSION_TYPES } from '../../constants';
|
||||
import {
|
||||
EXTENSION_NAME_REGEX,
|
||||
EXTENSION_PKG_KEY,
|
||||
HYBRID_EXTENSION_TYPES,
|
||||
NESTED_EXTENSION_TYPES,
|
||||
} from '../../constants';
|
||||
import { pluralize } from '../pluralize';
|
||||
import { validateExtensionManifest } from '../validate-extension-manifest';
|
||||
import { isIn, isTypeIn } from '../array-helpers';
|
||||
|
||||
async function resolvePackageExtensions(
|
||||
extensionNames: string[],
|
||||
root: string,
|
||||
types: readonly ExtensionPackageType[]
|
||||
): Promise<ExtensionPackage[]> {
|
||||
const extensions: ExtensionPackage[] = [];
|
||||
export async function resolvePackageExtensions(root: string, extensionNames?: string[]): Promise<Extension[]> {
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
const local = extensionNames === undefined;
|
||||
|
||||
if (extensionNames === undefined) {
|
||||
extensionNames = await listFolders(root);
|
||||
extensionNames = extensionNames.filter((name) => EXTENSION_NAME_REGEX.test(name));
|
||||
}
|
||||
|
||||
for (const extensionName of extensionNames) {
|
||||
const extensionPath = resolvePackage(extensionName, root);
|
||||
const extensionManifest: ExtensionManifestRaw = await fse.readJSON(path.join(extensionPath, 'package.json'));
|
||||
const extensionPath = local ? path.join(root, extensionName) : resolvePackage(extensionName, root);
|
||||
const extensionManifest: Record<string, any> = await fse.readJSON(path.join(extensionPath, 'package.json'));
|
||||
|
||||
if (!validateExtensionManifest(extensionManifest)) {
|
||||
throw new Error(`The extension manifest of "${extensionName}" is not valid.`);
|
||||
let parsedManifest;
|
||||
|
||||
try {
|
||||
parsedManifest = ExtensionManifest.parse(extensionManifest);
|
||||
} catch (error) {
|
||||
throw new Error(`The extension manifest of "${extensionName}" is not valid.\n${error}`);
|
||||
}
|
||||
|
||||
const extensionOptions = extensionManifest[EXTENSION_PKG_KEY];
|
||||
const extensionOptions = parsedManifest[EXTENSION_PKG_KEY];
|
||||
|
||||
if (isIn(extensionOptions.type, types)) {
|
||||
if (extensionOptions.type === 'pack') {
|
||||
const extensionChildren = Object.keys(extensionManifest.dependencies ?? {}).filter((dep) =>
|
||||
EXTENSION_NAME_REGEX.test(dep)
|
||||
);
|
||||
|
||||
const extension: ExtensionPackage = {
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
version: extensionManifest.version,
|
||||
type: extensionOptions.type,
|
||||
host: extensionOptions.host,
|
||||
children: extensionChildren,
|
||||
local: false,
|
||||
};
|
||||
|
||||
extensions.push(extension);
|
||||
extensions.push(...(await resolvePackageExtensions(extension.children || [], extension.path, types)));
|
||||
} else if (extensionOptions.type === 'bundle') {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
version: extensionManifest.version,
|
||||
type: extensionOptions.type,
|
||||
entrypoint: {
|
||||
app: extensionOptions.path.app,
|
||||
api: extensionOptions.path.api,
|
||||
},
|
||||
entries: extensionOptions.entries,
|
||||
host: extensionOptions.host,
|
||||
local: false,
|
||||
});
|
||||
} else if (isTypeIn(extensionOptions, HYBRID_EXTENSION_TYPES)) {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
version: extensionManifest.version,
|
||||
type: extensionOptions.type,
|
||||
entrypoint: {
|
||||
app: extensionOptions.path.app,
|
||||
api: extensionOptions.path.api,
|
||||
},
|
||||
host: extensionOptions.host,
|
||||
local: false,
|
||||
});
|
||||
} else {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
version: extensionManifest.version,
|
||||
type: extensionOptions.type,
|
||||
entrypoint: extensionOptions.path,
|
||||
host: extensionOptions.host,
|
||||
local: false,
|
||||
});
|
||||
}
|
||||
if (extensionOptions.type === 'bundle') {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: parsedManifest.name,
|
||||
version: parsedManifest.version,
|
||||
type: extensionOptions.type,
|
||||
entrypoint: {
|
||||
app: extensionOptions.path.app,
|
||||
api: extensionOptions.path.api,
|
||||
},
|
||||
entries: extensionOptions.entries,
|
||||
host: extensionOptions.host,
|
||||
local,
|
||||
});
|
||||
} else if (isTypeIn(extensionOptions, HYBRID_EXTENSION_TYPES)) {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: parsedManifest.name,
|
||||
version: parsedManifest.version,
|
||||
type: extensionOptions.type,
|
||||
entrypoint: {
|
||||
app: extensionOptions.path.app,
|
||||
api: extensionOptions.path.api,
|
||||
},
|
||||
host: extensionOptions.host,
|
||||
local,
|
||||
});
|
||||
} else {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: parsedManifest.name,
|
||||
version: parsedManifest.version,
|
||||
type: extensionOptions.type,
|
||||
entrypoint: extensionOptions.path,
|
||||
host: extensionOptions.host,
|
||||
local,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
export async function getPackageExtensions(
|
||||
root: string,
|
||||
types: readonly ExtensionPackageType[]
|
||||
): Promise<ExtensionPackage[]> {
|
||||
export async function getPackageExtensions(root: string): Promise<Extension[]> {
|
||||
let pkg: { dependencies?: Record<string, string> };
|
||||
|
||||
try {
|
||||
@@ -107,13 +90,13 @@ export async function getPackageExtensions(
|
||||
|
||||
const extensionNames = Object.keys(pkg.dependencies ?? {}).filter((dep) => EXTENSION_NAME_REGEX.test(dep));
|
||||
|
||||
return resolvePackageExtensions(extensionNames, root, types);
|
||||
return resolvePackageExtensions(root, extensionNames);
|
||||
}
|
||||
|
||||
export async function getLocalExtensions(root: string, types: readonly ExtensionType[]): Promise<ExtensionLocal[]> {
|
||||
const extensions: ExtensionLocal[] = [];
|
||||
export async function getLocalExtensions(root: string): Promise<Extension[]> {
|
||||
const extensions: Extension[] = [];
|
||||
|
||||
for (const extensionType of types) {
|
||||
for (const extensionType of NESTED_EXTENSION_TYPES) {
|
||||
const typeDir = pluralize(extensionType);
|
||||
const typePath = path.resolve(root, typeDir);
|
||||
|
||||
@@ -123,15 +106,7 @@ export async function getLocalExtensions(root: string, types: readonly Extension
|
||||
for (const extensionName of extensionNames) {
|
||||
const extensionPath = path.join(typePath, extensionName);
|
||||
|
||||
if (!isIn(extensionType, HYBRID_EXTENSION_TYPES)) {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
type: extensionType,
|
||||
entrypoint: 'index.js',
|
||||
local: true,
|
||||
});
|
||||
} else {
|
||||
if (isIn(extensionType, HYBRID_EXTENSION_TYPES)) {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
@@ -142,9 +117,17 @@ export async function getLocalExtensions(root: string, types: readonly Extension
|
||||
},
|
||||
local: true,
|
||||
});
|
||||
} else {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
type: extensionType as AppExtensionType | ApiExtensionType,
|
||||
entrypoint: 'index.js',
|
||||
local: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (e) {
|
||||
throw new Error(`Extension folder "${typePath}" couldn't be opened`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { validateExtensionManifest, validateExtensionOptionsBundleEntry } from './validate-extension-manifest';
|
||||
|
||||
describe('validateExtensionManifest', () => {
|
||||
it('returns false when passed item has no name or version', () => {
|
||||
const mockExtension = {};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has no EXTENSION_PKG_KEY', () => {
|
||||
const mockExtension = { name: 'test', version: '0.1' };
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has no type', () => {
|
||||
const mockExtension = { name: 'test', version: '0.1', 'directus:extension': {} };
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has an invalid type', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': { type: 'not_extension_type' },
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has no host', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': { type: 'pack' },
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has a type of bundle and has no path, entries or they have the wrong format', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': { type: 'bundle', host: '^9.0.0' },
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has a Hybrid type and has no path, source or they have the wrong format', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': { type: 'operation', host: '^9.0.0' },
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has an App or API type and has no path or source', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': { type: 'interface', host: '^9.0.0' },
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when passed a valid ExtensionManifest with an App or API type', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': { type: 'interface', path: './dist/index.js', source: './src/index.js', host: '^9.0.0' },
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when passed a valid ExtensionManifest with a Hybrid type', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': {
|
||||
type: 'operation',
|
||||
path: { app: './dist/app.js', api: './dist/api.js' },
|
||||
source: { app: './src/app.js', api: './src/api.js' },
|
||||
host: '^9.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when passed a valid ExtensionManifest with a type of bundle', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': {
|
||||
type: 'bundle',
|
||||
path: { app: './dist/app.js', api: './dist/api.js' },
|
||||
entries: [
|
||||
{ type: 'interface', name: 'test-interface', source: './src/test-interface/index.js' },
|
||||
{ type: 'hook', name: 'test-hook', source: './src/test-hook/index.js' },
|
||||
{
|
||||
type: 'operation',
|
||||
name: 'test-operation',
|
||||
source: { app: './src/test-operation/app.js', api: './src/test-operation/api.js' },
|
||||
},
|
||||
],
|
||||
host: '^9.0.0',
|
||||
},
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when passed a valid ExtensionManifest with a Package type', () => {
|
||||
const mockExtension = {
|
||||
name: 'test',
|
||||
version: '0.1',
|
||||
'directus:extension': { type: 'pack', host: '^9.0.0' },
|
||||
};
|
||||
|
||||
expect(validateExtensionManifest(mockExtension)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateExtensionOptionsBundleEntry', () => {
|
||||
it('returns false when passed item has no type', () => {
|
||||
const mockEntry = {};
|
||||
|
||||
expect(validateExtensionOptionsBundleEntry(mockEntry)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has an invalid type', () => {
|
||||
const mockEntry = { type: 'bundle' };
|
||||
|
||||
expect(validateExtensionOptionsBundleEntry(mockEntry)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has no name', () => {
|
||||
const mockEntry = { type: 'interface' };
|
||||
|
||||
expect(validateExtensionOptionsBundleEntry(mockEntry)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has a Hybrid type and has no source or it has the wrong format', () => {
|
||||
const mockEntry = { type: 'operation', name: 'test', source: './src/index.js' };
|
||||
|
||||
expect(validateExtensionOptionsBundleEntry(mockEntry)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when passed item has an App or API type and has no source', () => {
|
||||
const mockEntry = { type: 'interface', name: 'test' };
|
||||
|
||||
expect(validateExtensionOptionsBundleEntry(mockEntry)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when passed a valid ExtensionOptionsBundleEntry with an App or API type', () => {
|
||||
const mockEntry = { type: 'interface', name: 'test', source: './src/index.js' };
|
||||
|
||||
expect(validateExtensionOptionsBundleEntry(mockEntry)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when passed a valid ExtensionOptionsBundleEntry with a Hybrid type', () => {
|
||||
const mockEntry = { type: 'operation', name: 'test', source: { app: './src/app.js', api: './src/api.js' } };
|
||||
|
||||
expect(validateExtensionOptionsBundleEntry(mockEntry)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
import {
|
||||
EXTENSION_PACKAGE_TYPES,
|
||||
EXTENSION_PKG_KEY,
|
||||
EXTENSION_TYPES,
|
||||
HYBRID_EXTENSION_TYPES,
|
||||
PACKAGE_EXTENSION_TYPES,
|
||||
} from '../constants';
|
||||
import {
|
||||
ExtensionManifest,
|
||||
ExtensionManifestRaw,
|
||||
ExtensionOptionsBundleEntry,
|
||||
ExtensionOptionsBundleEntryRaw,
|
||||
} from '../types';
|
||||
import { isIn } from './array-helpers';
|
||||
|
||||
export function validateExtensionManifest(
|
||||
extensionManifest: ExtensionManifestRaw
|
||||
): extensionManifest is ExtensionManifest {
|
||||
if (!extensionManifest.name || !extensionManifest.version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionOptions = extensionManifest[EXTENSION_PKG_KEY];
|
||||
|
||||
if (
|
||||
!extensionOptions ||
|
||||
!extensionOptions.type ||
|
||||
!isIn(extensionOptions.type, EXTENSION_PACKAGE_TYPES) ||
|
||||
!extensionOptions.host
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isIn(extensionOptions.type, PACKAGE_EXTENSION_TYPES)) {
|
||||
if (extensionOptions.type === 'bundle') {
|
||||
if (
|
||||
!extensionOptions.path ||
|
||||
typeof extensionOptions.path === 'string' ||
|
||||
!extensionOptions.path.app ||
|
||||
!extensionOptions.path.api ||
|
||||
!extensionOptions.entries ||
|
||||
!Array.isArray(extensionOptions.entries) ||
|
||||
!extensionOptions.entries.every((entry) => validateExtensionOptionsBundleEntry(entry))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isIn(extensionOptions.type, HYBRID_EXTENSION_TYPES)) {
|
||||
if (
|
||||
!extensionOptions.path ||
|
||||
!extensionOptions.source ||
|
||||
typeof extensionOptions.path === 'string' ||
|
||||
typeof extensionOptions.source === 'string' ||
|
||||
!extensionOptions.path.app ||
|
||||
!extensionOptions.path.api ||
|
||||
!extensionOptions.source.app ||
|
||||
!extensionOptions.source.api
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!extensionOptions.path || !extensionOptions.source) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function validateExtensionOptionsBundleEntry(
|
||||
entry: ExtensionOptionsBundleEntryRaw
|
||||
): entry is ExtensionOptionsBundleEntry {
|
||||
if (!entry.type || !isIn(entry.type, EXTENSION_TYPES) || !entry.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isIn(entry.type, HYBRID_EXTENSION_TYPES)) {
|
||||
if (!entry.source || typeof entry.source === 'string' || !entry.source.app || !entry.source.api) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!entry.source) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -683,6 +683,7 @@ importers:
|
||||
vue: 3.2.45
|
||||
vue-i18n: 9.2.2
|
||||
vue-router: 4.1.6
|
||||
zod: ^3.20.2
|
||||
dependencies:
|
||||
axios: 1.2.1
|
||||
date-fns: 2.29.3
|
||||
@@ -699,6 +700,7 @@ importers:
|
||||
vue: 3.2.45
|
||||
vue-i18n: 9.2.2_vue@3.2.45
|
||||
vue-router: 4.1.6_vue@3.2.45
|
||||
zod: 3.20.2
|
||||
devDependencies:
|
||||
'@types/express': 4.17.15
|
||||
'@types/fs-extra': 9.0.13
|
||||
@@ -8732,6 +8734,7 @@ packages:
|
||||
|
||||
/bindings/1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
dev: true
|
||||
@@ -11520,6 +11523,7 @@ packages:
|
||||
|
||||
/file-uri-to-path/1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
@@ -15316,6 +15320,7 @@ packages:
|
||||
|
||||
/nan/2.17.0:
|
||||
resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
@@ -21353,6 +21358,10 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/zod/3.20.2:
|
||||
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}
|
||||
dev: false
|
||||
|
||||
/zwitch/1.0.5:
|
||||
resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
|
||||
dev: true
|
||||
|
||||
Reference in New Issue
Block a user