From 80bcc35af3dbc584b766e489b367568c7bd381a4 Mon Sep 17 00:00:00 2001 From: Nicola Krumschmidt Date: Tue, 2 May 2023 20:45:25 +0200 Subject: [PATCH] Add support for building API extensions to ESM (#18351) * Remove language check from extension build command * Add getFileExt util * Add support for building API extensions to ESM * Build new extensions to ESM by default * Update config file docs to be in ESM * Add changeset * Fix extension test * Replace nested ternary expression Co-authored-by: Brainslug * Li(n)t --------- Co-authored-by: Pascal Jufer Co-authored-by: Brainslug --- .changeset/light-laws-love.md | 7 + docs/extensions/creating-extensions.md | 2 +- packages/constants/src/extensions.ts | 1 + packages/extensions-sdk/create-build.test.ts | 2 +- .../extensions-sdk/src/cli/commands/build.ts | 129 +++++------------- .../extensions-sdk/src/cli/commands/create.ts | 1 + packages/extensions-sdk/src/cli/types.ts | 2 + packages/extensions-sdk/src/cli/utils/file.ts | 3 + .../extensions-sdk/src/cli/utils/languages.ts | 3 +- 9 files changed, 53 insertions(+), 97 deletions(-) create mode 100644 .changeset/light-laws-love.md create mode 100644 packages/extensions-sdk/src/cli/utils/file.ts diff --git a/.changeset/light-laws-love.md b/.changeset/light-laws-love.md new file mode 100644 index 0000000000..f9d27d5031 --- /dev/null +++ b/.changeset/light-laws-love.md @@ -0,0 +1,7 @@ +--- +'@directus/extensions-sdk': minor +'@directus/constants': minor +'docs': minor +--- + +Added support for building API extensions to ESM format and default to ESM for new extensions diff --git a/docs/extensions/creating-extensions.md b/docs/extensions/creating-extensions.md index eecc14095e..ea952af491 100644 --- a/docs/extensions/creating-extensions.md +++ b/docs/extensions/creating-extensions.md @@ -83,7 +83,7 @@ to your specific needs. This can be done by creating a `extension.config.js` fil with the following content: ```js -module.exports = { +export default { plugins: [], }; ``` diff --git a/packages/constants/src/extensions.ts b/packages/constants/src/extensions.ts index 0935ef66d6..0a1b5143cd 100644 --- a/packages/constants/src/extensions.ts +++ b/packages/constants/src/extensions.ts @@ -80,6 +80,7 @@ export const ExtensionOptions = ExtensionOptionsBase.and( export const ExtensionManifest = z.object({ name: z.string(), version: z.string(), + type: z.union([z.literal('module'), z.literal('commonjs')]).optional(), dependencies: z.record(z.string()).optional(), [EXTENSION_PKG_KEY]: ExtensionOptions, }); diff --git a/packages/extensions-sdk/create-build.test.ts b/packages/extensions-sdk/create-build.test.ts index afba3f978d..2755eba304 100644 --- a/packages/extensions-sdk/create-build.test.ts +++ b/packages/extensions-sdk/create-build.test.ts @@ -19,10 +19,10 @@ afterAll(async () => { function getConfigFileContent(configFileName: string) { switch (configFileName) { + case 'extension.config.js': case 'extension.config.mjs': return `export default { plugins: [] };`; case 'extension.config.cjs': - case 'extension.config.js': return `module.exports = { plugins: [] };`; default: return ''; diff --git a/packages/extensions-sdk/src/cli/commands/build.ts b/packages/extensions-sdk/src/cli/commands/build.ts index aec8bbbd81..c61861bd78 100644 --- a/packages/extensions-sdk/src/cli/commands/build.ts +++ b/packages/extensions-sdk/src/cli/commands/build.ts @@ -30,13 +30,13 @@ import { rollup, watch as rollupWatch } from 'rollup'; import esbuildDefault from 'rollup-plugin-esbuild'; import stylesDefault from 'rollup-plugin-styles'; import vueDefault from 'rollup-plugin-vue'; -import type { Language, RollupConfig, RollupMode } from '../types.js'; -import { getLanguageFromPath, isLanguage } from '../utils/languages.js'; +import type { Format, RollupConfig, RollupMode } from '../types.js'; import { clear, log } from '../utils/logger.js'; import tryParseJson from '../utils/try-parse-json.js'; import generateBundleEntrypoint from './helpers/generate-bundle-entrypoint.js'; import loadConfig from './helpers/load-config.js'; import { validateSplitEntrypointOption } from './helpers/validate-cli-options.js'; +import { getFileExt } from '../utils/file.js'; // Workaround for https://github.com/rollup/plugins/issues/1329 const virtual = virtualDefault as unknown as typeof virtualDefault.default; @@ -81,11 +81,14 @@ export default async function build(options: BuildOptions): Promise { const extensionOptions = extensionManifest[EXTENSION_PKG_KEY]; + const format = extensionManifest.type === 'module' ? 'esm' : 'cjs'; + if (extensionOptions.type === 'bundle') { await buildBundleExtension({ entries: extensionOptions.entries, outputApp: extensionOptions.path.app, outputApi: extensionOptions.path.api, + format, watch, sourcemap, minify, @@ -96,6 +99,7 @@ export default async function build(options: BuildOptions): Promise { inputApi: extensionOptions.source.api, outputApp: extensionOptions.path.app, outputApi: extensionOptions.path.api, + format, watch, sourcemap, minify, @@ -105,6 +109,7 @@ export default async function build(options: BuildOptions): Promise { type: extensionOptions.type, input: extensionOptions.source, output: extensionOptions.path, + format, watch, sourcemap, minify, @@ -175,6 +180,7 @@ export default async function build(options: BuildOptions): Promise { entries: entries.data, outputApp: splitOutput.app, outputApi: splitOutput.api, + format: 'esm', watch, sourcemap, minify, @@ -210,6 +216,7 @@ export default async function build(options: BuildOptions): Promise { inputApi: splitInput.api, outputApp: splitOutput.app, outputApi: splitOutput.api, + format: 'esm', watch, sourcemap, minify, @@ -219,6 +226,7 @@ export default async function build(options: BuildOptions): Promise { type, input, output, + format: 'esm', watch, sourcemap, minify, @@ -231,6 +239,7 @@ async function buildAppOrApiExtension({ type, input, output, + format, watch, sourcemap, minify, @@ -238,6 +247,7 @@ async function buildAppOrApiExtension({ type: AppExtensionType | ApiExtensionType; input: string; output: string; + format: Format; watch: boolean; sourcemap: boolean; minify: boolean; @@ -252,20 +262,13 @@ async function buildAppOrApiExtension({ process.exit(1); } - const language = getLanguageFromPath(input); - - if (!isLanguage(language)) { - log(`Language ${chalk.bold(language)} is not supported.`, 'error'); - process.exit(1); - } - const config = await loadConfig(); const plugins = config.plugins ?? []; const mode = isIn(type, APP_EXTENSION_TYPES) ? 'browser' : 'node'; - const rollupOptions = getRollupOptions({ mode, input, language, sourcemap, minify, plugins }); - const rollupOutputOptions = getRollupOutputOptions({ mode, output, sourcemap }); + const rollupOptions = getRollupOptions({ mode, input, sourcemap, minify, plugins }); + const rollupOutputOptions = getRollupOutputOptions({ mode, output, format, sourcemap }); if (watch) { await watchExtension({ rollupOptions, rollupOutputOptions }); @@ -279,6 +282,7 @@ async function buildHybridExtension({ inputApi, outputApp, outputApi, + format, watch, sourcemap, minify, @@ -287,6 +291,7 @@ async function buildHybridExtension({ inputApi: string; outputApp: string; outputApi: string; + format: Format; watch: boolean; sourcemap: boolean; minify: boolean; @@ -311,26 +316,12 @@ async function buildHybridExtension({ process.exit(1); } - const languageApp = getLanguageFromPath(inputApp); - const languageApi = getLanguageFromPath(inputApi); - - if (!isLanguage(languageApp)) { - log(`App language ${chalk.bold(languageApp)} is not supported.`, 'error'); - process.exit(1); - } - - if (!isLanguage(languageApi)) { - log(`API language ${chalk.bold(languageApi)} is not supported.`, 'error'); - process.exit(1); - } - const config = await loadConfig(); const plugins = config.plugins ?? []; const rollupOptionsApp = getRollupOptions({ mode: 'browser', input: inputApp, - language: languageApp, sourcemap, minify, plugins, @@ -339,14 +330,13 @@ async function buildHybridExtension({ const rollupOptionsApi = getRollupOptions({ mode: 'node', input: inputApi, - language: languageApi, sourcemap, minify, plugins, }); - const rollupOutputOptionsApp = getRollupOutputOptions({ mode: 'browser', output: outputApp, sourcemap }); - const rollupOutputOptionsApi = getRollupOutputOptions({ mode: 'node', output: outputApi, sourcemap }); + const rollupOutputOptionsApp = getRollupOutputOptions({ mode: 'browser', output: outputApp, format, sourcemap }); + const rollupOutputOptionsApi = getRollupOutputOptions({ mode: 'node', output: outputApi, format, sourcemap }); const rollupOptionsAll = [ { rollupOptions: rollupOptionsApp, rollupOutputOptions: rollupOutputOptionsApp }, @@ -364,6 +354,7 @@ async function buildBundleExtension({ entries, outputApp, outputApi, + format, watch, sourcemap, minify, @@ -371,6 +362,7 @@ async function buildBundleExtension({ entries: ExtensionOptionsBundleEntry[]; outputApp: string; outputApi: string; + format: Format; watch: boolean; sourcemap: boolean; minify: boolean; @@ -385,62 +377,6 @@ async function buildBundleExtension({ process.exit(1); } - const languagesApp = new Set(); - const languagesApi = new Set(); - - for (const entry of entries) { - if (isTypeIn(entry, HYBRID_EXTENSION_TYPES)) { - const inputApp = entry.source.app; - const inputApi = entry.source.api; - - if (!(await fse.pathExists(inputApp)) || !(await fse.stat(inputApp)).isFile()) { - log(`App entrypoint ${chalk.bold(inputApp)} does not exist.`, 'error'); - process.exit(1); - } - - if (!(await fse.pathExists(inputApi)) || !(await fse.stat(inputApi)).isFile()) { - log(`API entrypoint ${chalk.bold(inputApi)} does not exist.`, 'error'); - process.exit(1); - } - - const languageApp = getLanguageFromPath(inputApp); - const languageApi = getLanguageFromPath(inputApi); - - if (!isLanguage(languageApp)) { - log(`App language ${chalk.bold(languageApp)} is not supported.`, 'error'); - process.exit(1); - } - - if (!isLanguage(languageApi)) { - log(`API language ${chalk.bold(languageApi)} is not supported.`, 'error'); - process.exit(1); - } - - languagesApp.add(languageApp); - languagesApi.add(languageApi); - } else { - const input = entry.source; - - if (!(await fse.pathExists(input)) || !(await fse.stat(input)).isFile()) { - log(`Entrypoint ${chalk.bold(input)} does not exist.`, 'error'); - process.exit(1); - } - - const language = getLanguageFromPath(input); - - if (!isLanguage(language)) { - log(`Language ${chalk.bold(language)} is not supported.`, 'error'); - process.exit(1); - } - - if (isIn(entry.type, APP_EXTENSION_TYPES)) { - languagesApp.add(language); - } else { - languagesApi.add(language); - } - } - } - const config = await loadConfig(); const plugins = config.plugins ?? []; @@ -450,7 +386,6 @@ async function buildBundleExtension({ const rollupOptionsApp = getRollupOptions({ mode: 'browser', input: { entry: entrypointApp }, - language: Array.from(languagesApp), sourcemap, minify, plugins, @@ -459,14 +394,13 @@ async function buildBundleExtension({ const rollupOptionsApi = getRollupOptions({ mode: 'node', input: { entry: entrypointApi }, - language: Array.from(languagesApi), sourcemap, minify, plugins, }); - const rollupOutputOptionsApp = getRollupOutputOptions({ mode: 'browser', output: outputApp, sourcemap }); - const rollupOutputOptionsApi = getRollupOutputOptions({ mode: 'node', output: outputApi, sourcemap }); + const rollupOutputOptionsApp = getRollupOutputOptions({ mode: 'browser', output: outputApp, format, sourcemap }); + const rollupOutputOptionsApi = getRollupOutputOptions({ mode: 'node', output: outputApi, format, sourcemap }); const rollupOptionsAll = [ { rollupOptions: rollupOptionsApp, rollupOutputOptions: rollupOutputOptionsApp }, @@ -568,27 +502,23 @@ async function watchExtension(config: RollupConfig | RollupConfig[]) { function getRollupOptions({ mode, input, - language, sourcemap, minify, plugins, }: { mode: RollupMode; input: string | Record; - language: Language | Language[]; sourcemap: boolean; minify: boolean; plugins: Plugin[]; }): RollupOptions { - const languages = Array.isArray(language) ? language : [language]; - return { input: typeof input !== 'string' ? 'entry' : input, external: mode === 'browser' ? APP_SHARED_DEPS : API_SHARED_DEPS, plugins: [ typeof input !== 'string' ? virtual(input) : null, mode === 'browser' ? (vue({ preprocessStyles: true }) as Plugin) : null, - languages.includes('typescript') ? esbuild({ include: /\.tsx?$/, sourceMap: sourcemap }) : null, + esbuild({ include: /\.tsx?$/, sourceMap: sourcemap }), mode === 'browser' ? styles() : null, ...plugins, nodeResolve({ browser: mode === 'browser', preferBuiltins: mode === 'node' }), @@ -613,15 +543,26 @@ function getRollupOptions({ function getRollupOutputOptions({ mode, output, + format, sourcemap, }: { mode: RollupMode; output: string; + format: Format; sourcemap: boolean; }): RollupOutputOptions { + const fileExtension = getFileExt(output); + let outputFormat = format; + + if (mode === 'browser' || fileExtension === 'mjs') { + outputFormat = 'esm'; + } else if (fileExtension === 'cjs') { + outputFormat = 'cjs'; + } + return { file: output, - format: mode === 'browser' ? 'es' : 'cjs', + format: outputFormat, exports: 'auto', inlineDynamicImports: true, sourcemap, diff --git a/packages/extensions-sdk/src/cli/commands/create.ts b/packages/extensions-sdk/src/cli/commands/create.ts index dc48fc77ff..f20ee73e2f 100644 --- a/packages/extensions-sdk/src/cli/commands/create.ts +++ b/packages/extensions-sdk/src/cli/commands/create.ts @@ -170,6 +170,7 @@ function getPackageManifest(name: string, options: ExtensionOptions, deps: Recor icon: 'extension', version: '1.0.0', keywords: ['directus', 'directus-extension', `directus-custom-${options.type}`], + type: 'module', [EXTENSION_PKG_KEY]: options, scripts: { build: 'directus-extension build', diff --git a/packages/extensions-sdk/src/cli/types.ts b/packages/extensions-sdk/src/cli/types.ts index e0b580c514..5d3c4748bb 100644 --- a/packages/extensions-sdk/src/cli/types.ts +++ b/packages/extensions-sdk/src/cli/types.ts @@ -10,3 +10,5 @@ export type Config = { export type RollupConfig = { rollupOptions: RollupOptions; rollupOutputOptions: RollupOutputOptions }; export type RollupMode = 'browser' | 'node'; + +export type Format = 'esm' | 'cjs'; diff --git a/packages/extensions-sdk/src/cli/utils/file.ts b/packages/extensions-sdk/src/cli/utils/file.ts new file mode 100644 index 0000000000..aae56e5ab5 --- /dev/null +++ b/packages/extensions-sdk/src/cli/utils/file.ts @@ -0,0 +1,3 @@ +export function getFileExt(path: string): string { + return path.substring(path.lastIndexOf('.') + 1); +} diff --git a/packages/extensions-sdk/src/cli/utils/languages.ts b/packages/extensions-sdk/src/cli/utils/languages.ts index 121de1407d..2129fc0c45 100644 --- a/packages/extensions-sdk/src/cli/utils/languages.ts +++ b/packages/extensions-sdk/src/cli/utils/languages.ts @@ -1,5 +1,6 @@ import { EXTENSION_LANGUAGES } from '@directus/constants'; import type { Language, LanguageShort } from '../types.js'; +import { getFileExt } from './file.js'; export function isLanguage(language: string): language is Language { return (EXTENSION_LANGUAGES as readonly string[]).includes(language); @@ -14,7 +15,7 @@ export function languageToShort(language: Language): LanguageShort { } export function getLanguageFromPath(path: string): string { - const fileExtension = path.substring(path.lastIndexOf('.') + 1); + const fileExtension = getFileExt(path); if (fileExtension === 'js') { return 'javascript';