Skip to content

Commit

Permalink
Add create command to extension-sdk CLI (directus#6590)
Browse files Browse the repository at this point in the history
* Add create command to extension-sdk CLI

* Extract extension package.json key name to shared

* Check package.json before building extensions

* Add source field to package.json

* Pin extension-sdk verson in scaffolded package

* Change options color to magenta
  • Loading branch information
nickrum authored Jul 7, 2021
1 parent e7a737b commit ff393ea
Show file tree
Hide file tree
Showing 19 changed files with 277 additions and 8 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
dist
templates
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/extension-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@vue/compiler-sfc": "^3.1.1",
"chalk": "^4.1.1",
"commander": "^8.0.0",
"execa": "^5.1.1",
"fs-extra": "^10.0.0",
"ora": "^5.4.0",
"rollup": "^2.51.2",
"rollup-plugin-styles": "^3.14.1",
Expand Down
30 changes: 29 additions & 1 deletion packages/extension-sdk/src/cli/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
/* eslint-disable no-console */

import path from 'path';
import chalk from 'chalk';
import fse from 'fs-extra';
import ora from 'ora';
import { rollup } from 'rollup';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import styles from 'rollup-plugin-styles';
import vue from 'rollup-plugin-vue';
import { SHARED_DEPS } from '@directus/shared/constants';
import { APP_EXTENSION_TYPES, EXTENSION_PKG_KEY, SHARED_DEPS } from '@directus/shared/constants';

export default async function build(options: { input: string; output: string }): Promise<void> {
const packagePath = path.resolve('package.json');

if (!(await fse.pathExists(packagePath))) {
console.log(`${chalk.bold.red('[Error]')} Current directory is not a package.`);
process.exit(1);
}

const packageManifest = await fse.readJSON(packagePath);

if (!packageManifest[EXTENSION_PKG_KEY] || !packageManifest[EXTENSION_PKG_KEY].type) {
console.log(`${chalk.bold.yellow('[Warn]')} Current directory is not a Directus extension.`);
} else {
const type = packageManifest[EXTENSION_PKG_KEY].type;

if (!APP_EXTENSION_TYPES.includes(type)) {
console.log(
`${chalk.bold.yellow('[Warn]')} Extension type ${chalk.bold(
type
)} is not supported. Available extension types: ${APP_EXTENSION_TYPES.map((t) => chalk.bold.magenta(t)).join(
', '
)}.`
);
}
}

const spinner = ora('Building Directus extension...').start();

const bundle = await rollup({
Expand Down
84 changes: 84 additions & 0 deletions packages/extension-sdk/src/cli/commands/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* eslint-disable no-console */

import path from 'path';
import chalk from 'chalk';
import fse from 'fs-extra';
import execa from 'execa';
import ora from 'ora';
import { EXTENSION_TYPES, EXTENSION_PKG_KEY } from '@directus/shared/constants';
import { ExtensionType } from '@directus/shared/types';

const pkg = require('../../../../package.json');

const TEMPLATE_PATH = path.resolve(__dirname, '..', '..', '..', '..', 'templates');

export default async function create(type: ExtensionType, name: string): Promise<void> {
const targetPath = path.resolve(name);

if (!EXTENSION_TYPES.includes(type)) {
console.log(
`${chalk.bold.red('[Error]')} Extension type ${chalk.bold(
type
)} does not exist. Available extension types: ${EXTENSION_TYPES.map((t) => chalk.bold.magenta(t)).join(', ')}.`
);
process.exit(1);
}

if (await fse.pathExists(targetPath)) {
const info = await fse.stat(targetPath);

if (!info.isDirectory()) {
console.log(
`${chalk.bold.red('[Error]')} Destination ${chalk.bold(name)} already exists and is not a directory.`
);
process.exit(1);
}

const files = await fse.readdir(targetPath);

if (files.length > 0) {
console.log(`${chalk.bold.red('[Error]')} Destination ${chalk.bold(name)} already exists and is not empty.`);
process.exit(1);
}
}

const spinner = ora(`Scaffolding Directus extension...`).start();

await fse.ensureDir(targetPath);

await fse.copy(path.join(TEMPLATE_PATH, 'common'), targetPath);
await fse.copy(path.join(TEMPLATE_PATH, type), targetPath);

const packageManifest = {
name: `directus-extension-${name}`,
version: '1.0.0',
keywords: ['directus', 'directus-extension', `directus-custom-${type}`],
[EXTENSION_PKG_KEY]: {
type: type,
path: 'dist/index.js',
source: 'src/index.js',
host: `^${pkg.version}`,
hidden: false,
},
scripts: {
build: 'directus-extension build',
},
devDependencies: {
'@directus/extension-sdk': pkg.version,
},
};

await fse.writeJSON(path.join(targetPath, 'package.json'), packageManifest, { spaces: '\t' });

await execa('npm', ['install'], { cwd: targetPath });

spinner.succeed('Done');

console.log(`
Your ${type} extension has been created at ${chalk.green(targetPath)}
Build your extension by running:
${chalk.blue('cd')} ${name}
${chalk.blue('npm run')} build
`);
}
3 changes: 3 additions & 0 deletions packages/extension-sdk/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Command } from 'commander';
import create from './commands/create';
import build from './commands/build';

const pkg = require('../../../package.json');
Expand All @@ -8,6 +9,8 @@ const program = new Command();
program.name('directus-extension').usage('[command] [options]');
program.version(pkg.version, '-v, --version');

program.command('create').arguments('<type> <name>').description('Scaffold a new Directus extension').action(create);

program
.command('build')
.description('Bundle a Directus extension to a single entrypoint')
Expand Down
3 changes: 3 additions & 0 deletions packages/extension-sdk/templates/common/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
node_modules
dist
11 changes: 11 additions & 0 deletions packages/extension-sdk/templates/display/src/display.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<div>Value: {{ value }}</div>
</template>

<script>
export default {
props: {
value: String,
},
};
</script>
10 changes: 10 additions & 0 deletions packages/extension-sdk/templates/display/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import DisplayComponent from './display.vue';

export default {
id: 'custom',
name: 'Custom',
description: 'This is my custom display!',
icon: 'box',
handler: DisplayComponent,
types: ['string'],
};
3 changes: 3 additions & 0 deletions packages/extension-sdk/templates/endpoint/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function registerEndpoint(router) {
router.get('/', (req, res) => res.send('Hello, World!'));
};
9 changes: 9 additions & 0 deletions packages/extension-sdk/templates/hook/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const axios = require('axios');

module.exports = function registerHook() {
return {
'items.create': function () {
axios.post('http://example.com/webhook');
},
};
};
10 changes: 10 additions & 0 deletions packages/extension-sdk/templates/interface/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import InterfaceComponent from './interface.vue';

export default {
id: 'custom',
name: 'Custom',
description: 'This is my custom interface!',
icon: 'box',
component: InterfaceComponent,
types: ['string'],
};
17 changes: 17 additions & 0 deletions packages/extension-sdk/templates/interface/src/interface.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<input :value="value" @input="handleChange($event.target.value)" />
</template>

<script>
export default {
emits: ['input'],
props: {
value: String,
},
methods: {
handleChange(value) {
this.$emit('input', value);
},
},
};
</script>
19 changes: 19 additions & 0 deletions packages/extension-sdk/templates/layout/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ref } from 'vue';
import LayoutComponent from './layout.vue';

export default {
id: 'custom',
name: 'Custom',
icon: 'box',
component: LayoutComponent,
slots: {
options: () => null,
sidebar: () => null,
actions: () => null,
},
setup(props) {
const name = ref('Custom layout state');

return { name };
},
};
17 changes: 17 additions & 0 deletions packages/extension-sdk/templates/layout/src/layout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div>{{ name }} - Collection: {{ props.collection }}</div>
</template>

<script>
import { toRefs } from 'vue';
import { useLayoutState } from '@directus/extension-sdk';
export default {
setup() {
const layoutState = useLayoutState();
const { props, name } = toRefs(layoutState.value);
return { props, name };
},
};
</script>
13 changes: 13 additions & 0 deletions packages/extension-sdk/templates/module/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ModuleComponent from './module.vue';

export default {
id: 'custom',
name: 'Custom',
icon: 'box',
routes: [
{
path: '',
component: ModuleComponent,
},
],
};
7 changes: 7 additions & 0 deletions packages/extension-sdk/templates/module/src/module.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<private-view title="My Custom Module">Content goes here...</private-view>
</template>

<script>
export default {};
</script>
2 changes: 2 additions & 0 deletions packages/shared/src/constants/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export const API_EXTENSION_TYPES: ApiExtensionType[] = ['endpoint', 'hook'];
export const EXTENSION_TYPES: ExtensionType[] = [...APP_EXTENSION_TYPES, ...API_EXTENSION_TYPES];

export const EXTENSION_NAME_REGEX = /^(?:(?:@[^/]+\/)?directus-extension-|@directus\/extension-).+$/;

export const EXTENSION_PKG_KEY = 'directus:extension';
14 changes: 7 additions & 7 deletions packages/shared/src/utils/get-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fse from 'fs-extra';
import { Extension } from '../types';
import { resolvePackage } from './resolve-package';
import { listFolders } from './list-folders';
import { EXTENSION_NAME_REGEX, EXTENSION_TYPES } from '../constants';
import { EXTENSION_NAME_REGEX, EXTENSION_PKG_KEY, EXTENSION_TYPES } from '../constants';
import { pluralize } from './pluralize';

export async function getPackageExtensions(root: string): Promise<Extension[]> {
Expand All @@ -19,7 +19,7 @@ export async function getPackageExtensions(root: string): Promise<Extension[]> {
const extensionPath = resolvePackage(extensionName, root);
const extensionPkg = await fse.readJSON(path.join(extensionPath, 'package.json'));

if (extensionPkg['directus:extension'].type === 'pack') {
if (extensionPkg[EXTENSION_PKG_KEY].type === 'pack') {
const extensionChildren = Object.keys(extensionPkg.dependencies).filter((dep) =>
EXTENSION_NAME_REGEX.test(dep)
);
Expand All @@ -28,8 +28,8 @@ export async function getPackageExtensions(root: string): Promise<Extension[]> {
path: extensionPath,
name: extensionName,
version: extensionPkg.version,
type: extensionPkg['directus:extension'].type,
host: extensionPkg['directus:extension'].host,
type: extensionPkg[EXTENSION_PKG_KEY].type,
host: extensionPkg[EXTENSION_PKG_KEY].host,
children: extensionChildren,
local: false,
root: root === undefined,
Expand All @@ -42,9 +42,9 @@ export async function getPackageExtensions(root: string): Promise<Extension[]> {
path: extensionPath,
name: extensionName,
version: extensionPkg.version,
type: extensionPkg['directus:extension'].type,
entrypoint: extensionPkg['directus:extension'].path,
host: extensionPkg['directus:extension'].host,
type: extensionPkg[EXTENSION_PKG_KEY].type,
entrypoint: extensionPkg[EXTENSION_PKG_KEY].path,
host: extensionPkg[EXTENSION_PKG_KEY].host,
local: false,
root: root === undefined,
});
Expand Down

0 comments on commit ff393ea

Please sign in to comment.