[kbn/plugin-generator] remove sao, modernize (#75465)

Co-authored-by: spalger <spalger@users.noreply.github.com>
This commit is contained in:
Spencer 2020-08-20 18:50:36 -07:00 committed by GitHub
parent 958296c5c2
commit 7b23e7cd8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 4128 additions and 4529 deletions

View file

@ -41,7 +41,7 @@ target
# package overrides
/packages/eslint-config-kibana
/packages/kbn-interpreter/src/common/lib/grammar.js
/packages/kbn-plugin-generator/sao_template/template
/packages/kbn-plugin-generator/template
/packages/kbn-pm/dist
/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/
/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/

View file

@ -604,7 +604,6 @@ module.exports = {
{
files: [
'.eslintrc.js',
'packages/kbn-plugin-generator/**/*.js',
'packages/kbn-eslint-import-resolver-kibana/**/*.js',
'packages/kbn-eslint-plugin-eslint/**/*',
'x-pack/gulpfile.js',

View file

@ -17,17 +17,17 @@ If you are targeting **Kibana 6.3 or greater** then checkout the corresponding K
To target the current development version of Kibana just use the default `master` branch.
```sh
node scripts/generate_plugin my_plugin_name
node scripts/generate_plugin --name my_plugin_name -y
# generates a plugin in `plugins/my_plugin_name`
```
To target 6.3, use the `6.x` branch (until the `6.3` branch is created).
To target 6.8, use the `6.8` branch.
```sh
git checkout 6.x
yarn kbn bootstrap # always bootstrap when switching branches
node scripts/generate_plugin my_plugin_name
# generates a plugin for Kibana 6.3 in `../kibana-extra/my_plugin_name`
node scripts/generate_plugin --name my_plugin_name -y
# generates a plugin for Kibana 6.8 in `../kibana-extra/my_plugin_name`
```
The generate script supports a few flags; run it with the `--help` flag to learn more.
@ -49,7 +49,7 @@ yarn kbn bootstrap
## Plugin Development Scripts
Generated plugins receive a handful of scripts that can be used during development. Those scripts are detailed in the [README.md](sao_template/template/README.md) file in each newly generated plugin, and expose the scripts provided by the [Kibana plugin helpers](../kbn-plugin-helpers), but here is a quick reference in case you need it:
Generated plugins receive a handful of scripts that can be used during development. Those scripts are detailed in the [README.md](template/README.md) file in each newly generated plugin, and expose the scripts provided by the [Kibana plugin helpers](../kbn-plugin-helpers), but here is a quick reference in case you need it:
> ***NOTE:*** All of these scripts should be run from the generated plugin.

View file

@ -1,69 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const { resolve } = require('path');
const dedent = require('dedent');
const sao = require('sao');
const chalk = require('chalk');
const getopts = require('getopts');
const { snakeCase } = require('lodash');
exports.run = function run(argv) {
const options = getopts(argv, {
alias: {
h: 'help',
i: 'internal',
},
});
if (!options.help && options._.length !== 1) {
console.log(chalk`{red {bold [name]} is a required argument}\n`);
options.help = true;
}
if (options.help) {
console.log(
dedent(chalk`
# {dim Usage:}
node scripts/generate-plugin {bold [name]}
Generate a fresh Kibana plugin in the plugins/ directory
`) + '\n'
);
process.exit(1);
}
const name = options._[0];
const template = resolve(__dirname, './sao_template');
const kibanaPlugins = resolve(process.cwd(), 'plugins');
const targetPath = resolve(kibanaPlugins, snakeCase(name));
sao({
template: template,
targetPath: targetPath,
configOptions: {
name,
targetPath,
},
}).catch((error) => {
console.error(chalk`{red fatal error}!`);
console.error(error.stack);
process.exit(1);
});
};

View file

@ -1,62 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { spawn } from 'child_process';
import Fs from 'fs';
import { resolve } from 'path';
import { promisify } from 'util';
import del from 'del';
import { snakeCase } from 'lodash';
const statAsync = promisify(Fs.stat);
const ROOT_DIR = resolve(__dirname, '../../../');
const pluginName = 'ispec-plugin';
const snakeCased = snakeCase(pluginName);
const generatedPath = resolve(ROOT_DIR, `plugins/${snakeCased}`);
beforeAll(async () => {
await del(generatedPath, { force: true });
});
afterAll(async () => {
await del(generatedPath, { force: true });
});
it('generates a plugin', async () => {
await new Promise((resolve, reject) => {
const proc = spawn(process.execPath, ['scripts/generate_plugin.js', pluginName], {
cwd: ROOT_DIR,
stdio: 'pipe',
});
proc.stdout.on('data', function selectDefaults() {
proc.stdin.write('\n'); // Generate a plugin with default options.
});
proc.on('close', resolve);
proc.on('error', reject);
});
const stats = await statAsync(generatedPath);
if (!stats.isDirectory()) {
throw new Error(`Expected [${generatedPath}] to be a directory`);
}
});

View file

@ -1,14 +1,26 @@
{
"name": "@kbn/plugin-generator",
"license": "Apache-2.0",
"private": true,
"version": "1.0.0",
"private": true,
"license": "Apache-2.0",
"main": "target/index.js",
"scripts": {
"kbn:bootstrap": "node scripts/build",
"kbn:watch": "node scripts/build --watch"
},
"dependencies": {
"chalk": "^4.1.0",
"dedent": "^0.7.0",
"@kbn/dev-utils": "1.0.0",
"ejs": "^3.1.5",
"execa": "^4.0.2",
"getopts": "^2.2.4",
"lodash": "^4.17.15",
"sao": "^0.22.12"
"inquirer": "^7.3.3",
"normalize-path": "^3.0.0",
"prettier": "^2.0.5",
"vinyl": "^2.2.0",
"vinyl-fs": "^3.0.3"
},
"devDependencies": {
"@types/ejs": "^3.0.4",
"@types/prettier": "^2.0.2",
"@types/inquirer": "^7.3.1"
}
}

View file

@ -1,189 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const { relative, resolve } = require('path');
const fs = require('fs');
const { camelCase, startCase, snakeCase } = require('lodash');
const chalk = require('chalk');
const execa = require('execa');
const pkg = require('../package.json');
const kibanaPkgPath = require.resolve('../../../package.json');
const kibanaPkg = require(kibanaPkgPath); // eslint-disable-line import/no-dynamic-require
async function gitInit(dir) {
// Only plugins in /plugins get git init
try {
await execa('git', ['init', dir]);
console.log(`Git repo initialized in ${dir}`);
} catch (error) {
console.error(error);
throw new Error(`Failure to git init ${dir}: ${error.all || error}`);
}
}
async function moveToCustomFolder(from, to) {
try {
await execa('mv', [from, to]);
} catch (error) {
console.error(error);
throw new Error(`Failure to move plugin to ${to}: ${error.all || error}`);
}
}
async function eslintPlugin(dir) {
try {
await execa('yarn', ['lint:es', `./${dir}/**/*.ts*`, '--no-ignore', '--fix']);
} catch (error) {
console.error(error);
throw new Error(`Failure when running prettier on the generated output: ${error.all || error}`);
}
}
module.exports = function ({ name, targetPath }) {
return {
prompts: {
customPath: {
message: 'Would you like to create the plugin in a different folder?',
default: '/plugins',
filter(value) {
// Keep default value empty
if (value === '/plugins') return '';
// Remove leading slash
return value.startsWith('/') ? value.slice(1) : value;
},
validate(customPath) {
const p = resolve(process.cwd(), customPath);
const exists = fs.existsSync(p);
if (!exists)
return `Folder should exist relative to the kibana root folder. Consider /src/plugins or /x-pack/plugins.`;
return true;
},
},
description: {
message: 'Provide a short description',
default: 'An awesome Kibana plugin',
},
kbnVersion: {
message: 'What Kibana version are you targeting?',
default: kibanaPkg.version,
},
generateApp: {
type: 'confirm',
message: 'Should an app component be generated?',
default: true,
},
generateApi: {
type: 'confirm',
message: 'Should a server API be generated?',
default: true,
},
generateTranslations: {
type: 'confirm',
when: (answers) => {
// only for 3rd party plugins
return !answers.customPath && answers.generateApp;
},
message: 'Should translation files be generated?',
default({ customPath }) {
// only for 3rd party plugins
return !customPath;
},
},
generateScss: {
type: 'confirm',
message: 'Should SCSS be used?',
when: (answers) => answers.generateApp,
default: true,
},
generateEslint: {
type: 'confirm',
message: 'Would you like to use a custom eslint file?',
default({ customPath }) {
return !customPath;
},
},
generateTsconfig: {
type: 'confirm',
message: 'Would you like to use a custom tsconfig file?',
default: true,
},
},
filters: {
'public/**/index.scss': 'generateScss',
'public/**/*': 'generateApp',
'server/**/*': 'generateApi',
'translations/**/*': 'generateTranslations',
'i18nrc.json': 'generateTranslations',
'eslintrc.js': 'generateEslint',
'tsconfig.json': 'generateTsconfig',
},
move: {
'eslintrc.js': '.eslintrc.js',
'i18nrc.json': '.i18nrc.json',
},
data: (answers) => {
const pathToPlugin = answers.customPath
? resolve(answers.customPath, camelCase(name), 'public')
: resolve(targetPath, 'public');
return Object.assign(
{
templateVersion: pkg.version,
startCase,
camelCase,
snakeCase,
name,
// kibana plugins are placed in a the non default path
isKibanaPlugin: !answers.customPath,
kbnVersion: answers.kbnVersion,
upperCamelCaseName: name.charAt(0).toUpperCase() + camelCase(name).slice(1),
hasUi: !!answers.generateApp,
hasServer: !!answers.generateApi,
hasScss: !!answers.generateScss,
relRoot: relative(pathToPlugin, process.cwd()),
},
answers
);
},
enforceNewFolder: true,
installDependencies: false,
async post({ log, answers }) {
let dir = relative(process.cwd(), targetPath);
if (answers.customPath) {
// Move to custom path
moveToCustomFolder(targetPath, answers.customPath);
dir = relative(process.cwd(), resolve(answers.customPath, snakeCase(name)));
} else {
// Init git only in the default path
await gitInit(dir);
}
// Apply eslint to the generated plugin
eslintPlugin(dir);
log.success(chalk`🎉
Your plugin has been created in {bold ${dir}}.
{bold yarn start}
`);
},
};
};

View file

@ -1,159 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const sao = require('sao');
const template = {
fromPath: __dirname,
configOptions: {
name: 'Some fancy plugin',
targetPath: '',
},
};
function getFileContents(file) {
return file.contents.toString();
}
describe('plugin generator sao integration', () => {
test('skips files when answering no', async () => {
const res = await sao.mockPrompt(template, {
generateApp: false,
generateApi: false,
});
expect(res.fileList).toContain('common/index.ts');
expect(res.fileList).not.toContain('public/index.ts');
expect(res.fileList).not.toContain('server/index.ts');
});
it('includes app when answering yes', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateApi: false,
generateScss: true,
});
// check output files
expect(res.fileList).toContain('common/index.ts');
expect(res.fileList).toContain('public/index.ts');
expect(res.fileList).toContain('public/plugin.ts');
expect(res.fileList).toContain('public/types.ts');
expect(res.fileList).toContain('public/components/app.tsx');
expect(res.fileList).toContain('public/index.scss');
expect(res.fileList).not.toContain('server/index.ts');
});
it('includes server api when answering yes', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateApi: true,
});
// check output files
expect(res.fileList).toContain('public/plugin.ts');
expect(res.fileList).toContain('server/plugin.ts');
expect(res.fileList).toContain('server/index.ts');
expect(res.fileList).toContain('server/types.ts');
expect(res.fileList).toContain('server/routes/index.ts');
});
it('skips eslintrc and scss', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateApi: true,
generateScss: false,
generateEslint: false,
generateTsconfig: false,
});
// check output files
expect(res.fileList).toContain('public/plugin.ts');
expect(res.fileList).not.toContain('public/index.scss');
expect(res.fileList).not.toContain('.eslintrc.js');
expect(res.fileList).not.toContain('tsconfig.json');
});
it('plugin package has correct title', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateApi: true,
});
const contents = getFileContents(res.files['common/index.ts']);
const controllerLine = contents.match("PLUGIN_NAME = '(.*)'")[1];
expect(controllerLine).toContain('Some fancy plugin');
});
it('package has version "kibana" with master', async () => {
const res = await sao.mockPrompt(template, {
kbnVersion: 'master',
});
const packageContents = getFileContents(res.files['kibana.json']);
const pkg = JSON.parse(packageContents);
expect(pkg.version).toBe('master');
});
it('package has correct version', async () => {
const res = await sao.mockPrompt(template, {
kbnVersion: 'v6.0.0',
});
const packageContents = getFileContents(res.files['kibana.json']);
const pkg = JSON.parse(packageContents);
expect(pkg.version).toBe('v6.0.0');
});
it('sample app has correct values', async () => {
const res = await sao.mockPrompt(template, {
generateApp: true,
generateApi: true,
});
const contents = getFileContents(res.files['common/index.ts']);
const controllerLine = contents.match("PLUGIN_ID = '(.*)'")[1];
expect(controllerLine).toContain('someFancyPlugin');
});
it('includes dotfiles', async () => {
const res = await sao.mockPrompt(template);
expect(res.files['tsconfig.json']).toBeTruthy();
expect(res.files['.eslintrc.js']).toBeTruthy();
expect(res.files['.i18nrc.json']).toBeTruthy();
});
it('validaes path override', async () => {
try {
await sao.mockPrompt(template, {
generateApp: true,
generateApi: true,
generateScss: false,
generateEslint: false,
customPath: 'banana',
});
} catch (e) {
expect(e.message).toContain('Validation failed at prompt "customPath"');
}
});
});

View file

@ -1,9 +0,0 @@
module.exports = {
root: true,
extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'],
<%_ if (!isKibanaPlugin) { -%>
rules: {
"@kbn/eslint/require-license-header": "off"
}
<%_ } -%>
};

View file

@ -1,10 +0,0 @@
{
"prefix": "<%= camelCase(name) %>",
"paths": {
"<%= camelCase(name) %>": "."
},
"translations": [
"translations/ja-JP.json"
]
}

View file

@ -1,25 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from '<%= relRoot %>/src/core/public';
import { AppPluginStartDependencies } from './types';
import { <%= upperCamelCaseName %>App } from './components/app';
export const renderApp = (
{ notifications, http }: CoreStart,
{ navigation }: AppPluginStartDependencies,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<<%= upperCamelCaseName %>App
basename={appBasePath}
notifications={notifications}
http={http}
navigation={navigation}
/>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -1,16 +0,0 @@
<%_ if (hasScss) { -%>
import './index.scss';
<%_ } -%>
import { <%= upperCamelCaseName %>Plugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin() {
return new <%= upperCamelCaseName %>Plugin();
}
export {
<%= upperCamelCaseName %>PluginSetup,
<%= upperCamelCaseName %>PluginStart,
} from './types';

View file

@ -1,11 +0,0 @@
import { NavigationPublicPluginStart } from '<%= relRoot %>/src/plugins/navigation/public';
export interface <%= upperCamelCaseName %>PluginSetup {
getGreeting: () => string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface <%= upperCamelCaseName %>PluginStart {}
export interface AppPluginStartDependencies {
navigation: NavigationPublicPluginStart
};

View file

@ -1,15 +0,0 @@
import { PluginInitializerContext } from '<%= relRoot %>/src/core/server';
import { <%= upperCamelCaseName %>Plugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin(initializerContext: PluginInitializerContext) {
return new <%= upperCamelCaseName %>Plugin(initializerContext);
}
export {
<%= upperCamelCaseName %>PluginSetup,
<%= upperCamelCaseName %>PluginStart,
} from './types';

View file

@ -1,30 +0,0 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '<%= relRoot %>/src/core/server';
import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart } from './types';
import { defineRoutes } from './routes';
export class <%= upperCamelCaseName %>Plugin
implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> {
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
public setup(core: CoreSetup) {
this.logger.debug('<%= name %>: Setup');
const router = core.http.createRouter();
// Register server side APIs
defineRoutes(router);
return {};
}
public start(core: CoreStart) {
this.logger.debug('<%= name %>: Started');
return {};
}
public stop() {}
}

View file

@ -1,82 +0,0 @@
{
"formats": {
"number": {
"currency": {
"style": "currency"
},
"percent": {
"style": "percent"
}
},
"date": {
"short": {
"month": "numeric",
"day": "numeric",
"year": "2-digit"
},
"medium": {
"month": "short",
"day": "numeric",
"year": "numeric"
},
"long": {
"month": "long",
"day": "numeric",
"year": "numeric"
},
"full": {
"weekday": "long",
"month": "long",
"day": "numeric",
"year": "numeric"
}
},
"time": {
"short": {
"hour": "numeric",
"minute": "numeric"
},
"medium": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric"
},
"long": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
},
"full": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
}
},
"relative": {
"years": {
"units": "year"
},
"months": {
"units": "month"
},
"days": {
"units": "day"
},
"hours": {
"units": "hour"
},
"minutes": {
"units": "minute"
},
"seconds": {
"units": "second"
}
}
},
"messages": {
"<%= camelCase(name) %>.buttonText": "Translate me to Japanese",
}
}

View file

@ -0,0 +1,43 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const Path = require('path');
const { run } = require('@kbn/dev-utils');
const del = require('del');
const execa = require('execa');
run(
async ({ flags }) => {
await del(Path.resolve(__dirname, '../target'));
await execa(require.resolve('typescript/bin/tsc'), flags.watch ? ['--watch'] : [], {
cwd: Path.resolve(__dirname, '..'),
stdio: 'inherit',
});
},
{
flags: {
boolean: ['watch'],
help: `
--watch Watch files and rebuild on changes
`,
},
}
);

View file

@ -0,0 +1,103 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Path from 'path';
import { REPO_ROOT } from '@kbn/dev-utils';
import inquirer from 'inquirer';
export interface Answers {
name: string;
internal: boolean;
internalLocation: string;
ui: boolean;
server: boolean;
}
export const INTERNAL_PLUGIN_LOCATIONS: Array<{ name: string; value: string }> = [
{
name: 'Kibana Example',
value: Path.resolve(REPO_ROOT, 'examples'),
},
{
name: 'Kibana OSS',
value: Path.resolve(REPO_ROOT, 'src/plugins'),
},
{
name: 'Kibana OSS Functional Testing',
value: Path.resolve(REPO_ROOT, 'test/plugin_functional/plugins'),
},
{
name: 'X-Pack',
value: Path.resolve(REPO_ROOT, 'x-pack/plugins'),
},
{
name: 'X-Pack Functional Testing',
value: Path.resolve(REPO_ROOT, 'x-pack/test/plugin_functional/plugins'),
},
];
export const QUESTIONS = [
{
name: 'name',
message: 'Plugin name (use camelCase)',
default: undefined,
validate: (name: string) => (!name ? 'name is required' : true),
},
{
name: 'internal',
type: 'confirm',
message: 'Will this plugin be part of the Kibana repository?',
default: false,
},
{
name: 'internalLocation',
type: 'list',
message: 'What type of internal plugin would you like to create',
choices: INTERNAL_PLUGIN_LOCATIONS,
default: INTERNAL_PLUGIN_LOCATIONS[0].value,
when: ({ internal }: Answers) => internal,
},
{
name: 'ui',
type: 'confirm',
message: 'Should an UI plugin be generated?',
default: true,
},
{
name: 'server',
type: 'confirm',
message: 'Should a server plugin be generated?',
default: true,
},
] as const;
export async function askQuestions(overrides: Partial<Answers>) {
return await inquirer.prompt<Answers>(QUESTIONS, overrides);
}
export function getDefaultAnswers(overrides: Partial<Answers>) {
return QUESTIONS.reduce(
(acc, q) => ({
...acc,
[q.name]: overrides[q.name] != null ? overrides[q.name] : q.default,
}),
{}
) as Answers;
}

View file

@ -0,0 +1,68 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { camelCase, snakeCase, upperCamelCase } from './casing';
describe('camelCase', () => {
it.each([
['foo', 'foo'],
['foo_bar', 'fooBar'],
['foo bar', 'fooBar'],
['fooBar', 'fooBar'],
['___foo *$( bar 14', 'fooBar14'],
['foo-bar', 'fooBar'],
['FOO BAR', 'fooBar'],
['FOO_BAR', 'fooBar'],
['FOOBAR', 'foobar'],
])('converts %j to %j', (input, output) => {
expect(camelCase(input)).toBe(output);
});
});
describe('upperCamelCase', () => {
it.each([
['foo', 'Foo'],
['foo_bar', 'FooBar'],
['foo bar', 'FooBar'],
['fooBar', 'FooBar'],
['___foo *$( bar 14', 'FooBar14'],
['foo-bar', 'FooBar'],
['FOO BAR', 'FooBar'],
['FOO_BAR', 'FooBar'],
['FOOBAR', 'Foobar'],
])('converts %j to %j', (input, output) => {
expect(upperCamelCase(input)).toBe(output);
});
});
describe('snakeCase', () => {
it.each([
['foo', 'foo'],
['foo_bar', 'foo_bar'],
['foo bar', 'foo_bar'],
['fooBar', 'foo_bar'],
['___foo *$( bar 14', 'foo_bar_14'],
['foo-bar', 'foo_bar'],
['FOO BAR', 'foo_bar'],
['FOO_BAR', 'foo_bar'],
['FOOBAR', 'foobar'],
])('converts %j to %j', (input, output) => {
expect(snakeCase(input)).toBe(output);
});
});

View file

@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const words = (input: string) =>
input
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.toLowerCase()
.split(/[^a-z0-9]+/g)
.filter(Boolean);
const upperFirst = (input: string) => `${input.slice(0, 1).toUpperCase()}${input.slice(1)}`;
const lowerFirst = (input: string) => `${input.slice(0, 1).toLowerCase()}${input.slice(1)}`;
export const snakeCase = (input: string) => words(input).join('_');
export const upperCamelCase = (input: string) => words(input).map(upperFirst).join('');
export const camelCase = (input: string) => lowerFirst(upperCamelCase(input));

View file

@ -0,0 +1,98 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Path from 'path';
import Fs from 'fs';
import execa from 'execa';
import { REPO_ROOT, run, createFailError, createFlagError } from '@kbn/dev-utils';
import { snakeCase } from './casing';
import { askQuestions, getDefaultAnswers } from './ask_questions';
import { renderTemplates } from './render_template';
export function runCli() {
run(
async ({ log, flags }) => {
const name = flags.name || undefined;
if (name && typeof name !== 'string') {
throw createFlagError(`expected one --name flag`);
}
if (flags.yes && !name) {
throw createFlagError(`passing --yes requires that you specify a name`);
}
const overrides = {
name,
ui: typeof flags.ui === 'boolean' ? flags.ui : undefined,
server: typeof flags.server === 'boolean' ? flags.server : undefined,
};
const answers = flags.yes ? getDefaultAnswers(overrides) : await askQuestions(overrides);
const outputDir = answers.internal
? Path.resolve(answers.internalLocation, snakeCase(answers.name))
: Path.resolve(REPO_ROOT, 'plugins', snakeCase(answers.name));
if (Fs.existsSync(outputDir)) {
throw createFailError(`Target output directory [${outputDir}] already exists`);
}
// process the template directory, creating the actual plugin files
await renderTemplates({
outputDir,
answers,
});
// init git repo in third party plugins
if (!answers.internal) {
await execa('git', ['init', outputDir]);
}
log.success(
`🎉\n\nYour plugin has been created in ${Path.relative(process.cwd(), outputDir)}\n`
);
},
{
usage: 'node scripts/generate_plugin',
description: `
Generate a fresh Kibana plugin in the plugins/ directory
`,
flags: {
string: ['name'],
boolean: ['yes', 'ui', 'server'],
default: {
ui: null,
server: null,
},
alias: {
y: 'yes',
u: 'ui',
s: 'server',
},
help: `
--yes, -y Answer yes to all prompts, requires passing --name
--name Set the plugin name
--ui Generate a UI plugin
--server Generate a Server plugin
`,
},
}
);
}

View file

@ -16,9 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
interface PluginGenerator {
/**
* Run plugin generator.
*/
run: (...args: any[]) => any;
}
export * from './cli';

View file

@ -0,0 +1,143 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Path from 'path';
import del from 'del';
import execa from 'execa';
import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils';
import globby from 'globby';
const GENERATED_DIR = Path.resolve(REPO_ROOT, `plugins`);
expect.addSnapshotSerializer(createAbsolutePathSerializer());
beforeEach(async () => {
await del(GENERATED_DIR, { force: true });
});
afterEach(async () => {
await del(GENERATED_DIR, { force: true });
});
it('generates a plugin', async () => {
await execa(process.execPath, ['scripts/generate_plugin.js', '-y', '--name=foo'], {
cwd: REPO_ROOT,
buffer: true,
});
const paths = await globby('**/*', {
cwd: GENERATED_DIR,
absolute: true,
dot: true,
onlyFiles: true,
ignore: ['**/.git'],
});
expect(paths.sort((a, b) => a.localeCompare(b))).toMatchInlineSnapshot(`
Array [
<absolute path>/plugins/foo/.eslintrc.js,
<absolute path>/plugins/foo/.gitignore,
<absolute path>/plugins/foo/.i18nrc.json,
<absolute path>/plugins/foo/common/index.ts,
<absolute path>/plugins/foo/kibana.json,
<absolute path>/plugins/foo/package.json,
<absolute path>/plugins/foo/public/application.tsx,
<absolute path>/plugins/foo/public/components/app.tsx,
<absolute path>/plugins/foo/public/index.scss,
<absolute path>/plugins/foo/public/index.ts,
<absolute path>/plugins/foo/public/plugin.ts,
<absolute path>/plugins/foo/public/types.ts,
<absolute path>/plugins/foo/README.md,
<absolute path>/plugins/foo/server/index.ts,
<absolute path>/plugins/foo/server/plugin.ts,
<absolute path>/plugins/foo/server/routes/index.ts,
<absolute path>/plugins/foo/server/types.ts,
<absolute path>/plugins/foo/translations/ja-JP.json,
<absolute path>/plugins/foo/tsconfig.json,
]
`);
});
it('generates a plugin without UI', async () => {
await execa(process.execPath, ['scripts/generate_plugin.js', '--name=bar', '-y', '--no-ui'], {
cwd: REPO_ROOT,
buffer: true,
});
const paths = await globby('**/*', {
cwd: GENERATED_DIR,
absolute: true,
dot: true,
onlyFiles: true,
ignore: ['**/.git'],
});
expect(paths.sort((a, b) => a.localeCompare(b))).toMatchInlineSnapshot(`
Array [
<absolute path>/plugins/bar/.eslintrc.js,
<absolute path>/plugins/bar/.gitignore,
<absolute path>/plugins/bar/.i18nrc.json,
<absolute path>/plugins/bar/common/index.ts,
<absolute path>/plugins/bar/kibana.json,
<absolute path>/plugins/bar/package.json,
<absolute path>/plugins/bar/README.md,
<absolute path>/plugins/bar/server/index.ts,
<absolute path>/plugins/bar/server/plugin.ts,
<absolute path>/plugins/bar/server/routes/index.ts,
<absolute path>/plugins/bar/server/types.ts,
<absolute path>/plugins/bar/tsconfig.json,
]
`);
});
it('generates a plugin without server plugin', async () => {
await execa(process.execPath, ['scripts/generate_plugin.js', '--name=baz', '-y', '--no-server'], {
cwd: REPO_ROOT,
buffer: true,
});
const paths = await globby('**/*', {
cwd: GENERATED_DIR,
absolute: true,
dot: true,
onlyFiles: true,
ignore: ['**/.git'],
});
expect(paths.sort((a, b) => a.localeCompare(b))).toMatchInlineSnapshot(`
Array [
<absolute path>/plugins/baz/.eslintrc.js,
<absolute path>/plugins/baz/.gitignore,
<absolute path>/plugins/baz/.i18nrc.json,
<absolute path>/plugins/baz/common/index.ts,
<absolute path>/plugins/baz/kibana.json,
<absolute path>/plugins/baz/package.json,
<absolute path>/plugins/baz/public/application.tsx,
<absolute path>/plugins/baz/public/components/app.tsx,
<absolute path>/plugins/baz/public/index.scss,
<absolute path>/plugins/baz/public/index.ts,
<absolute path>/plugins/baz/public/plugin.ts,
<absolute path>/plugins/baz/public/types.ts,
<absolute path>/plugins/baz/README.md,
<absolute path>/plugins/baz/translations/ja-JP.json,
<absolute path>/plugins/baz/tsconfig.json,
]
`);
});

View file

@ -0,0 +1,127 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Path from 'path';
import { pipeline } from 'stream';
import { promisify } from 'util';
import vfs from 'vinyl-fs';
import prettier from 'prettier';
import { REPO_ROOT } from '@kbn/dev-utils';
import ejs from 'ejs';
import { snakeCase, camelCase, upperCamelCase } from './casing';
import { excludeFiles, tapFileStream } from './streams';
import { Answers } from './ask_questions';
const asyncPipeline = promisify(pipeline);
/**
* Stream all the files from the template directory, ignoring
* certain files based on the answers, process the .ejs templates
* to the output files they represent, renaming the .ejs files to
* remove that extension, then run every file through prettier
* before writing the files to the output directory.
*/
export async function renderTemplates({
outputDir,
answers,
}: {
outputDir: string;
answers: Answers;
}) {
const prettierConfig = await prettier.resolveConfig(process.cwd());
const defaultTemplateData = {
name: answers.name,
internalPlugin: !!answers.internal,
thirdPartyPlugin: !answers.internal,
hasServer: !!answers.server,
hasUi: !!answers.ui,
camelCase,
snakeCase,
upperCamelCase,
};
await asyncPipeline(
vfs.src(['**/*'], {
dot: true,
buffer: true,
nodir: true,
cwd: Path.resolve(__dirname, '../template'),
}),
// exclude files from the template based on selected options, patterns
// are matched without the .ejs extension
excludeFiles(
([] as string[]).concat(
answers.ui ? [] : 'public/**/*',
answers.ui && !answers.internal ? [] : ['translations/**/*', 'i18nrc.json'],
answers.server ? [] : 'server/**/*',
!answers.internal ? [] : ['eslintrc.js', 'tsconfig.json', 'package.json', '.gitignore']
)
),
// render .ejs templates and rename to not use .ejs extension
tapFileStream((file) => {
if (file.extname !== '.ejs') {
return;
}
const templateData = {
...defaultTemplateData,
importFromRoot(rootRelative: string) {
const filesOutputDirname = Path.dirname(Path.resolve(outputDir, file.relative));
const target = Path.resolve(REPO_ROOT, rootRelative);
return Path.relative(filesOutputDirname, target);
},
};
// render source and write back to file object
file.contents = Buffer.from(
ejs.render(file.contents.toString('utf8'), templateData, {
beautify: false,
})
);
// file.stem is the basename but without the extension
file.basename = file.stem;
}),
// format each file with prettier
tapFileStream((file) => {
if (!file.extname) {
return;
}
file.contents = Buffer.from(
prettier.format(file.contents.toString('utf8'), {
...prettierConfig,
filepath: file.path,
})
);
}),
// write files to disk
vfs.dest(outputDir)
);
}

View file

@ -0,0 +1,73 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Transform } from 'stream';
import File from 'vinyl';
import { Minimatch } from 'minimatch';
interface BufferedFile extends File {
contents: Buffer;
isDirectory(): false;
}
/**
* Create a transform stream that processes Vinyl fs streams and
* calls a function for each file, allowing the function to either
* mutate the file, replace it with another file (return a new File
* object), or drop it from the stream (return null)
*/
export const tapFileStream = (
fn: (file: BufferedFile) => File | void | null | Promise<File | void | null>
) =>
new Transform({
objectMode: true,
transform(file: BufferedFile, _, cb) {
Promise.resolve(file)
.then(fn)
.then(
(result) => {
// drop the file when null is returned
if (result === null) {
cb();
} else {
cb(undefined, result || file);
}
},
(error) => cb(error)
);
},
});
export const excludeFiles = (globs: string[]) => {
const patterns = globs.map(
(g) =>
new Minimatch(g, {
matchBase: true,
})
);
return tapFileStream((file) => {
const path = file.relative.replace(/\.ejs$/, '');
const exclude = patterns.some((p) => p.match(path));
if (exclude) {
return null;
}
});
};

View file

@ -0,0 +1,10 @@
module.exports = {
root: true,
extends: [
'@elastic/eslint-config-kibana',
'plugin:@elastic/eui/recommended'
],
rules: {
'@kbn/eslint/require-license-header': 'off',
},
};

View file

@ -0,0 +1,2 @@
/build
/target

View file

@ -0,0 +1,9 @@
{
"prefix": "<%= camelCase(name) %>",
"paths": {
"<%= camelCase(name) %>": "."
},
"translations": [
"translations/ja-JP.json"
]
}

View file

@ -1,8 +1,6 @@
# <%= name %>
<%- (description || '').split('\n').map(function (line) {
return '> ' + line
}).join('\n') %>
A Kibana plugin
---

View file

@ -1,6 +1,7 @@
{
"id": "<%= camelCase(name) %>",
"version": "<%= kbnVersion %>",
"version": "1.0.0",
"kibanaVersion": "kibana",
"server": <%= hasServer %>,
"ui": <%= hasUi %>,
"requiredPlugins": ["navigation"],

View file

@ -0,0 +1,8 @@
{
"name": "<%= camelCase(name) %>",
"version": "0.0.0",
"private": true,
"scripts": {
"kbn": "node ../../scripts/kbn"
}
}

View file

@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from '<%= importFromRoot('src/core/public') %>';
import { AppPluginStartDependencies } from './types';
import { <%= upperCamelCase(name) %>App } from './components/app';
export const renderApp = (
{ notifications, http }: CoreStart,
{ navigation }: AppPluginStartDependencies,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<<%= upperCamelCase(name) %>App
basename={appBasePath}
notifications={notifications}
http={http}
navigation={navigation}
/>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -1,22 +1,3 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
@ -35,36 +16,36 @@ import {
EuiText,
} from '@elastic/eui';
import { CoreStart } from '<%= relRoot %>/../src/core/public';
import { NavigationPublicPluginStart } from '<%= relRoot %>/../src/plugins/navigation/public';
import { CoreStart } from '<%= importFromRoot('src/core/public') %>';
import { NavigationPublicPluginStart } from '<%= importFromRoot('src/plugins/navigation/public') %>';
import { PLUGIN_ID, PLUGIN_NAME } from '../../common';
interface <%= upperCamelCaseName %>AppDeps {
interface <%= upperCamelCase(name) %>AppDeps {
basename: string;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
navigation: NavigationPublicPluginStart;
}
export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, navigation }: <%= upperCamelCaseName %>AppDeps) => {
export const <%= upperCamelCase(name) %>App = ({ basename, notifications, http, navigation }: <%= upperCamelCase(name) %>AppDeps) => {
// Use React hooks to manage state.
const [timestamp, setTimestamp] = useState<string | undefined>();
const onClickHandler = () => {
<%_ if (generateApi) { -%>
// Use the core http service to make a response to the server API.
http.get('/api/<%= snakeCase(name) %>/example').then(res => {
setTimestamp(res.time);
// Use the core notifications service to display a success message.
notifications.toasts.addSuccess(i18n.translate('<%= camelCase(name) %>.dataUpdated', {
defaultMessage: 'Data updated',
}));
});
<%_ } else { -%>
setTimestamp(new Date().toISOString());
notifications.toasts.addSuccess(PLUGIN_NAME);
<%_ } -%>
<% if (hasServer) { %>
// Use the core http service to make a response to the server API.
http.get('/api/<%= snakeCase(name) %>/example').then(res => {
setTimestamp(res.time);
// Use the core notifications service to display a success message.
notifications.toasts.addSuccess(i18n.translate('<%= camelCase(name) %>.dataUpdated', {
defaultMessage: 'Data updated',
}));
});
<% } else { %>
setTimestamp(new Date().toISOString());
notifications.toasts.addSuccess(PLUGIN_NAME);
<% } %>
};
// Render the application DOM.
@ -115,7 +96,10 @@ export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, na
/>
</p>
<EuiButton type="primary" size="s" onClick={onClickHandler}>
<FormattedMessage id="<%= camelCase(name) %>.buttonText" defaultMessage="<%_ if (generateApi) { -%>Get data<%_ } else { -%>Click me<%_ } -%>" />
<FormattedMessage
id="<%= camelCase(name) %>.buttonText"
defaultMessage="<%= hasServer ? 'Get data' : 'Click me' %>"
/>
</EuiButton>
</EuiText>
</EuiPageContentBody>

View file

@ -0,0 +1,14 @@
import './index.scss';
import { <%= upperCamelCase(name) %>Plugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin() {
return new <%= upperCamelCase(name) %>Plugin();
}
export {
<%= upperCamelCase(name) %>PluginSetup,
<%= upperCamelCase(name) %>PluginStart,
} from './types';

View file

@ -1,12 +1,12 @@
import { i18n } from '@kbn/i18n';
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '<%= relRoot %>/src/core/public';
import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart, AppPluginStartDependencies } from './types';
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '<%= importFromRoot('src/core/public') %>';
import { <%= upperCamelCase(name) %>PluginSetup, <%= upperCamelCase(name) %>PluginStart, AppPluginStartDependencies } from './types';
import { PLUGIN_NAME } from '../common';
export class <%= upperCamelCaseName %>Plugin
implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> {
public setup(core: CoreSetup): <%= upperCamelCaseName %>PluginSetup {
export class <%= upperCamelCase(name) %>Plugin
implements Plugin<<%= upperCamelCase(name) %>PluginSetup, <%= upperCamelCase(name) %>PluginStart> {
public setup(core: CoreSetup): <%= upperCamelCase(name) %>PluginSetup {
// Register an application into the side navigation menu
core.application.register({
id: '<%= camelCase(name) %>',
@ -34,7 +34,7 @@ export class <%= upperCamelCaseName %>Plugin
};
}
public start(core: CoreStart): <%= upperCamelCaseName %>PluginStart {
public start(core: CoreStart): <%= upperCamelCase(name) %>PluginStart {
return {};
}

View file

@ -0,0 +1,11 @@
import { NavigationPublicPluginStart } from '<%= importFromRoot('src/plugins/navigation/public') %>';
export interface <%= upperCamelCase(name) %>PluginSetup {
getGreeting: () => string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface <%= upperCamelCase(name) %>PluginStart {}
export interface AppPluginStartDependencies {
navigation: NavigationPublicPluginStart
};

View file

@ -0,0 +1,15 @@
import { PluginInitializerContext } from '<%= importFromRoot('src/core/server') %>';
import { <%= upperCamelCase(name) %>Plugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin(initializerContext: PluginInitializerContext) {
return new <%= upperCamelCase(name) %>Plugin(initializerContext);
}
export {
<%= upperCamelCase(name) %>PluginSetup,
<%= upperCamelCase(name) %>PluginStart,
} from './types';

View file

@ -0,0 +1,40 @@
import {
PluginInitializerContext,
CoreSetup,
CoreStart,
Plugin,
Logger
} from '<%= importFromRoot('src/core/server') %>';
import {
<%= upperCamelCase(name) %>PluginSetup,
<%= upperCamelCase(name) %>PluginStart
} from './types';
import { defineRoutes } from './routes';
export class <%= upperCamelCase(name) %>Plugin
implements Plugin<<%= upperCamelCase(name) %>PluginSetup, <%= upperCamelCase(name) %>PluginStart> {
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
public setup(core: CoreSetup) {
this.logger.debug('<%= name %>: Setup');
const router = core.http.createRouter();
// Register server side APIs
defineRoutes(router);
return {};
}
public start(core: CoreStart) {
this.logger.debug('<%= name %>: Started');
return {};
}
public stop() {}
}

View file

@ -1,4 +1,5 @@
import { IRouter } from '<%= relRoot %>/../src/core/server';
import { IRouter } from '<%= importFromRoot('src/core/server') %>';
export function defineRoutes(router: IRouter) {
router.get(

View file

@ -1,4 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface <%= upperCamelCaseName %>PluginSetup {}
export interface <%= upperCamelCase(name) %>PluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface <%= upperCamelCaseName %>PluginStart {}
export interface <%= upperCamelCase(name) %>PluginStart {}

View file

@ -0,0 +1,81 @@
{
"formats": {
"number": {
"currency": {
"style": "currency"
},
"percent": {
"style": "percent"
}
},
"date": {
"short": {
"month": "numeric",
"day": "numeric",
"year": "2-digit"
},
"medium": {
"month": "short",
"day": "numeric",
"year": "numeric"
},
"long": {
"month": "long",
"day": "numeric",
"year": "numeric"
},
"full": {
"weekday": "long",
"month": "long",
"day": "numeric",
"year": "numeric"
}
},
"time": {
"short": {
"hour": "numeric",
"minute": "numeric"
},
"medium": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric"
},
"long": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
},
"full": {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short"
}
},
"relative": {
"years": {
"units": "year"
},
"months": {
"units": "month"
},
"days": {
"units": "day"
},
"hours": {
"units": "hour"
},
"minutes": {
"units": "minute"
},
"seconds": {
"units": "second"
}
}
},
"messages": {
"<%= camelCase(name) %>.buttonText": "Translate me to Japanese",
}
}

View file

@ -1,5 +1,12 @@
{
"extends": "../../tsconfig.json",
"include": ["**/*", "index.js.d.ts"],
"exclude": ["sao_template/template/*"]
"compilerOptions": {
"outDir": "target",
"target": "ES2019",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["src/template/*"]
}

File diff suppressed because one or more lines are too long

View file

@ -17,5 +17,5 @@
* under the License.
*/
require('../src/setup_node_env');
require('@kbn/plugin-generator').run(process.argv.slice(2));
require('../src/setup_node_env/prebuilt_dev_only_entry');
require('@kbn/plugin-generator').runCli();

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { dirname, extname, join, relative, resolve, sep } from 'path';
import { dirname, extname, join, relative, resolve, sep, basename } from 'path';
export class File {
private path: string;
@ -38,6 +38,12 @@ export class File {
return this.relativePath;
}
public getWithoutExtension() {
const directory = dirname(this.path);
const stem = basename(this.path, this.ext);
return new File(resolve(directory, stem));
}
public isJs() {
return this.ext === '.js';
}

View file

@ -47,7 +47,7 @@ export async function extractUntrackedMessagesTask({
'**/build/**',
'**/__fixtures__/**',
'**/packages/kbn-i18n/**',
'**/packages/kbn-plugin-generator/sao_template/**',
'**/packages/kbn-plugin-generator/template/**',
'**/packages/kbn-ui-framework/generator-kui/**',
'**/target/**',
'**/test/**',

View file

@ -109,6 +109,14 @@ export const IGNORE_DIRECTORY_GLOBS = [
'packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack',
];
/**
* These patterns identify files which should have the extension stripped
* to reveal the actual name that should be checked.
*
* @type {Array}
*/
export const REMOVE_EXTENSION = ['packages/kbn-plugin-generator/template/**/*.ejs'];
/**
* DO NOT ADD FILES TO THIS LIST!!
*

View file

@ -29,6 +29,7 @@ import {
IGNORE_FILE_GLOBS,
TEMPORARILY_IGNORED_PATHS,
KEBAB_CASE_DIRECTORY_GLOBS,
REMOVE_EXTENSION,
} from './casing_check_config';
const NON_SNAKE_CASE_RE = /[A-Z \-]/;
@ -143,6 +144,10 @@ async function checkForSnakeCase(log, files) {
}
export async function checkFileCasing(log, files) {
files = files.map((f) =>
matchesAnyGlob(f.getRelativePath(), REMOVE_EXTENSION) ? f.getWithoutExtension() : f
);
await checkForKebabCase(log, files);
await checkForSnakeCase(log, files);
}

653
yarn.lock

File diff suppressed because it is too large Load diff