mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[kbn/generate] refactor to better support new commands (#127382)
This commit is contained in:
parent
0e99e9430c
commit
9fc23fa8bd
6 changed files with 276 additions and 172 deletions
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-generate'],
|
||||
};
|
||||
|
|
|
@ -6,180 +6,26 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Fsp from 'fs/promises';
|
||||
import Path from 'path';
|
||||
import { RunWithCommands } from '@kbn/dev-utils';
|
||||
|
||||
import Ejs from 'ejs';
|
||||
import globby from 'globby';
|
||||
import { REPO_ROOT } from '@kbn/utils';
|
||||
import {
|
||||
RunWithCommands,
|
||||
createFlagError,
|
||||
createFailError,
|
||||
isFailError,
|
||||
sortPackageJson,
|
||||
} from '@kbn/dev-utils';
|
||||
import { discoverBazelPackages, generatePackagesBuildBazelFile } from '@kbn/bazel-packages';
|
||||
import normalizePath from 'normalize-path';
|
||||
import { Render } from './lib/render';
|
||||
import { ContextExtensions } from './generate_command';
|
||||
|
||||
const ROOT_PKG_DIR = Path.resolve(REPO_ROOT, 'packages');
|
||||
const TEMPLATE_DIR = Path.resolve(__dirname, '../templates/package');
|
||||
|
||||
const jsonHelper = (arg: any) => JSON.stringify(arg, null, 2);
|
||||
const jsHelper = (arg: string) => {
|
||||
if (typeof arg !== 'string') {
|
||||
throw new Error('js() only supports strings right now');
|
||||
}
|
||||
|
||||
const hasSingle = arg.includes(`'`);
|
||||
const hasBacktick = arg.includes('`');
|
||||
|
||||
if (!hasSingle) {
|
||||
return `'${arg}'`;
|
||||
}
|
||||
|
||||
if (!hasBacktick) {
|
||||
return `\`${arg}\``;
|
||||
}
|
||||
|
||||
return `'${arg.replaceAll(`'`, `\\'`)}'`;
|
||||
};
|
||||
import { PackageCommand } from './commands/package_command';
|
||||
|
||||
/**
|
||||
* Runs the generate CLI. Called by `node scripts/generate` and not intended for use outside of that script
|
||||
*/
|
||||
export function runGenerateCli() {
|
||||
new RunWithCommands({
|
||||
description: 'Run generators for different components in Kibana',
|
||||
})
|
||||
.command({
|
||||
name: 'package',
|
||||
description: 'Generate a basic package',
|
||||
usage: 'node scripts/generate package [name]',
|
||||
flags: {
|
||||
boolean: ['web', 'force', 'dev'],
|
||||
string: ['dir'],
|
||||
help: `
|
||||
--dev Generate a package which is intended for dev-only use and can access things like devDependencies
|
||||
--web Build webpack-compatible version of sources for this package. If your package is intended to be
|
||||
used in the browser and Node.js then you need to opt-into these sources being created.
|
||||
--dir Directory where this package will live, defaults to [./packages]
|
||||
--force If the packageDir already exists, delete it before generation
|
||||
`,
|
||||
new RunWithCommands<ContextExtensions>(
|
||||
{
|
||||
description: 'Run generators for different components in Kibana',
|
||||
extendContext(context) {
|
||||
return {
|
||||
render: new Render(context.log),
|
||||
};
|
||||
},
|
||||
async run({ log, flags }) {
|
||||
const [name] = flags._;
|
||||
if (!name) {
|
||||
throw createFlagError(`missing package name`);
|
||||
}
|
||||
if (!name.startsWith('@kbn/')) {
|
||||
throw createFlagError(`package name must start with @kbn/`);
|
||||
}
|
||||
|
||||
const typePkgName = `@types/${name.slice(1).replace('/', '__')}`;
|
||||
const web = !!flags.web;
|
||||
const dev = !!flags.dev;
|
||||
|
||||
const containingDir = flags.dir ? Path.resolve(`${flags.dir}`) : ROOT_PKG_DIR;
|
||||
const packageDir = Path.resolve(containingDir, name.slice(1).replace('/', '-'));
|
||||
const repoRelativeDir = normalizePath(Path.relative(REPO_ROOT, packageDir));
|
||||
|
||||
try {
|
||||
await Fsp.readdir(packageDir);
|
||||
if (!!flags.force) {
|
||||
await Fsp.rm(packageDir, { recursive: true });
|
||||
log.warning('deleted existing package at', packageDir);
|
||||
} else {
|
||||
throw createFailError(
|
||||
`Package dir [${packageDir}] already exists, either choose a new package name, or pass --force to delete the package and regenerate it`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isFailError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const templateFiles = await globby('**/*', {
|
||||
cwd: TEMPLATE_DIR,
|
||||
absolute: false,
|
||||
dot: true,
|
||||
onlyFiles: true,
|
||||
});
|
||||
|
||||
await Fsp.mkdir(packageDir, { recursive: true });
|
||||
|
||||
for (const rel of templateFiles) {
|
||||
const destDir = Path.resolve(packageDir, Path.dirname(rel));
|
||||
|
||||
await Fsp.mkdir(destDir, { recursive: true });
|
||||
|
||||
if (Path.basename(rel) === '.empty') {
|
||||
log.debug('created dir', destDir);
|
||||
// ignore .empty files in the template, just create the directory
|
||||
continue;
|
||||
}
|
||||
|
||||
const ejs = !!rel.endsWith('.ejs');
|
||||
const src = Path.resolve(TEMPLATE_DIR, rel);
|
||||
const dest = Path.resolve(packageDir, ejs ? rel.slice(0, -4) : rel);
|
||||
|
||||
if (!ejs) {
|
||||
await Fsp.copyFile(src, dest);
|
||||
log.debug('copied', rel);
|
||||
continue;
|
||||
}
|
||||
|
||||
const vars = {
|
||||
pkg: {
|
||||
name,
|
||||
web,
|
||||
dev,
|
||||
directoryName: Path.basename(repoRelativeDir),
|
||||
repoRelativeDir,
|
||||
},
|
||||
|
||||
// helpers
|
||||
json: jsonHelper,
|
||||
js: jsHelper,
|
||||
relativePathTo: (rootRelativePath: string) => {
|
||||
return Path.relative(Path.dirname(dest), Path.resolve(REPO_ROOT, rootRelativePath));
|
||||
},
|
||||
};
|
||||
|
||||
log.verbose('rendering', src, 'with variables', vars);
|
||||
let content = await Ejs.renderFile(src, vars);
|
||||
|
||||
if (Path.basename(dest) === 'package.json') {
|
||||
content = sortPackageJson(content);
|
||||
}
|
||||
|
||||
await Fsp.writeFile(dest, content);
|
||||
log.debug('rendered', rel);
|
||||
}
|
||||
|
||||
log.info('Wrote plugin files to', packageDir);
|
||||
|
||||
const packageJsonPath = Path.resolve(REPO_ROOT, 'package.json');
|
||||
const packageJson = JSON.parse(await Fsp.readFile(packageJsonPath, 'utf8'));
|
||||
|
||||
const [addDeps, removeDeps] = dev
|
||||
? [packageJson.devDependencies, packageJson.dependencies]
|
||||
: [packageJson.dependencies, packageJson.devDependencies];
|
||||
|
||||
addDeps[name] = `link:bazel-bin/${repoRelativeDir}`;
|
||||
addDeps[typePkgName] = `link:bazel-bin/${repoRelativeDir}/npm_module_types`;
|
||||
delete removeDeps[name];
|
||||
delete removeDeps[typePkgName];
|
||||
|
||||
await Fsp.writeFile(packageJsonPath, sortPackageJson(JSON.stringify(packageJson)));
|
||||
log.info('Updated package.json file');
|
||||
|
||||
await Fsp.writeFile(
|
||||
Path.resolve(REPO_ROOT, 'packages/BUILD.bazel'),
|
||||
generatePackagesBuildBazelFile(await discoverBazelPackages())
|
||||
);
|
||||
log.info('Updated packages/BUILD.bazel');
|
||||
|
||||
log.success(`Generated ${name}! Please bootstrap to make sure it works.`);
|
||||
},
|
||||
})
|
||||
.execute();
|
||||
},
|
||||
[PackageCommand]
|
||||
).execute();
|
||||
}
|
||||
|
|
141
packages/kbn-generate/src/commands/package_command.ts
Normal file
141
packages/kbn-generate/src/commands/package_command.ts
Normal file
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Fsp from 'fs/promises';
|
||||
import Path from 'path';
|
||||
|
||||
import normalizePath from 'normalize-path';
|
||||
import globby from 'globby';
|
||||
|
||||
import { REPO_ROOT } from '@kbn/utils';
|
||||
import { discoverBazelPackages, generatePackagesBuildBazelFile } from '@kbn/bazel-packages';
|
||||
import { createFailError, createFlagError, isFailError, sortPackageJson } from '@kbn/dev-utils';
|
||||
|
||||
import { ROOT_PKG_DIR, PKG_TEMPLATE_DIR } from '../paths';
|
||||
import type { GenerateCommand } from '../generate_command';
|
||||
|
||||
export const PackageCommand: GenerateCommand = {
|
||||
name: 'package',
|
||||
description: 'Generate a basic package',
|
||||
usage: 'node scripts/generate package [name]',
|
||||
flags: {
|
||||
boolean: ['web', 'force', 'dev'],
|
||||
string: ['dir'],
|
||||
help: `
|
||||
--dev Generate a package which is intended for dev-only use and can access things like devDependencies
|
||||
--web Build webpack-compatible version of sources for this package. If your package is intended to be
|
||||
used in the browser and Node.js then you need to opt-into these sources being created.
|
||||
--dir Directory where this package will live, defaults to [./packages]
|
||||
--force If the packageDir already exists, delete it before generation
|
||||
`,
|
||||
},
|
||||
async run({ log, flags, render }) {
|
||||
const [name] = flags._;
|
||||
if (!name) {
|
||||
throw createFlagError(`missing package name`);
|
||||
}
|
||||
if (!name.startsWith('@kbn/')) {
|
||||
throw createFlagError(`package name must start with @kbn/`);
|
||||
}
|
||||
|
||||
const typePkgName = `@types/${name.slice(1).replace('/', '__')}`;
|
||||
const web = !!flags.web;
|
||||
const dev = !!flags.dev;
|
||||
|
||||
const containingDir = flags.dir ? Path.resolve(`${flags.dir}`) : ROOT_PKG_DIR;
|
||||
const packageDir = Path.resolve(containingDir, name.slice(1).replace('/', '-'));
|
||||
const repoRelativeDir = normalizePath(Path.relative(REPO_ROOT, packageDir));
|
||||
|
||||
try {
|
||||
await Fsp.readdir(packageDir);
|
||||
if (!!flags.force) {
|
||||
await Fsp.rm(packageDir, { recursive: true });
|
||||
log.warning('deleted existing package at', packageDir);
|
||||
} else {
|
||||
throw createFailError(
|
||||
`Package dir [${packageDir}] already exists, either choose a new package name, or pass --force to delete the package and regenerate it`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isFailError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const templateFiles = await globby('**/*', {
|
||||
cwd: PKG_TEMPLATE_DIR,
|
||||
absolute: false,
|
||||
dot: true,
|
||||
onlyFiles: true,
|
||||
});
|
||||
|
||||
if (!templateFiles.length) {
|
||||
throw new Error('unable to find package template files');
|
||||
}
|
||||
|
||||
await Fsp.mkdir(packageDir, { recursive: true });
|
||||
|
||||
for (const rel of templateFiles) {
|
||||
const destDir = Path.resolve(packageDir, Path.dirname(rel));
|
||||
|
||||
await Fsp.mkdir(destDir, { recursive: true });
|
||||
|
||||
if (Path.basename(rel) === '.empty') {
|
||||
log.debug('created dir', destDir);
|
||||
// ignore .empty files in the template, just create the directory
|
||||
continue;
|
||||
}
|
||||
|
||||
const ejs = !!rel.endsWith('.ejs');
|
||||
const src = Path.resolve(PKG_TEMPLATE_DIR, rel);
|
||||
const dest = Path.resolve(packageDir, ejs ? rel.slice(0, -4) : rel);
|
||||
|
||||
if (!ejs) {
|
||||
// read+write rather than `Fsp.copyFile` so that permissions of bazel-out are not copied to target
|
||||
await Fsp.writeFile(dest, await Fsp.readFile(src));
|
||||
log.debug('copied', rel);
|
||||
continue;
|
||||
}
|
||||
|
||||
await render.toFile(src, dest, {
|
||||
pkg: {
|
||||
name,
|
||||
web,
|
||||
dev,
|
||||
directoryName: Path.basename(repoRelativeDir),
|
||||
repoRelativeDir,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
log.info('Wrote plugin files to', packageDir);
|
||||
|
||||
const packageJsonPath = Path.resolve(REPO_ROOT, 'package.json');
|
||||
const packageJson = JSON.parse(await Fsp.readFile(packageJsonPath, 'utf8'));
|
||||
|
||||
const [addDeps, removeDeps] = dev
|
||||
? [packageJson.devDependencies, packageJson.dependencies]
|
||||
: [packageJson.dependencies, packageJson.devDependencies];
|
||||
|
||||
addDeps[name] = `link:bazel-bin/${repoRelativeDir}`;
|
||||
addDeps[typePkgName] = `link:bazel-bin/${repoRelativeDir}/npm_module_types`;
|
||||
delete removeDeps[name];
|
||||
delete removeDeps[typePkgName];
|
||||
|
||||
await Fsp.writeFile(packageJsonPath, sortPackageJson(JSON.stringify(packageJson)));
|
||||
log.info('Updated package.json file');
|
||||
|
||||
await Fsp.writeFile(
|
||||
Path.resolve(REPO_ROOT, 'packages/BUILD.bazel'),
|
||||
generatePackagesBuildBazelFile(await discoverBazelPackages())
|
||||
);
|
||||
log.info('Updated packages/BUILD.bazel');
|
||||
|
||||
log.success(`Generated ${name}! Please bootstrap to make sure it works.`);
|
||||
},
|
||||
};
|
17
packages/kbn-generate/src/generate_command.ts
Normal file
17
packages/kbn-generate/src/generate_command.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Command } from '@kbn/dev-utils';
|
||||
|
||||
import { Render } from './lib/render';
|
||||
|
||||
export interface ContextExtensions {
|
||||
render: Render;
|
||||
}
|
||||
|
||||
export type GenerateCommand = Command<ContextExtensions>;
|
85
packages/kbn-generate/src/lib/render.ts
Normal file
85
packages/kbn-generate/src/lib/render.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Path from 'path';
|
||||
import Fsp from 'fs/promises';
|
||||
|
||||
import Ejs from 'ejs';
|
||||
import normalizePath from 'normalize-path';
|
||||
import { ToolingLog, sortPackageJson } from '@kbn/dev-utils';
|
||||
import { REPO_ROOT } from '@kbn/utils';
|
||||
|
||||
export type Vars = Record<string, unknown>;
|
||||
export interface RenderContext extends Vars {
|
||||
/**
|
||||
* convert any serializable value into prett-printed JSON
|
||||
*/
|
||||
json(arg: any): string;
|
||||
/**
|
||||
* convert string values into valid and pretty JS
|
||||
*/
|
||||
js(arg: string): string;
|
||||
/**
|
||||
* create a normalized relative path from the generated files location to a repo-relative path
|
||||
*/
|
||||
relativePathTo(rootRelativePath: string): string;
|
||||
}
|
||||
|
||||
export class Render {
|
||||
jsonHelper: RenderContext['json'] = (arg) => JSON.stringify(arg, null, 2);
|
||||
jsHelper: RenderContext['js'] = (arg) => {
|
||||
if (typeof arg !== 'string') {
|
||||
throw new Error('js() only supports strings right now');
|
||||
}
|
||||
|
||||
const hasSingle = arg.includes(`'`);
|
||||
const hasBacktick = arg.includes('`');
|
||||
|
||||
if (!hasSingle) {
|
||||
return `'${arg}'`;
|
||||
}
|
||||
|
||||
if (!hasBacktick) {
|
||||
return `\`${arg}\``;
|
||||
}
|
||||
|
||||
return `'${arg.replaceAll(`'`, `\\'`)}'`;
|
||||
};
|
||||
|
||||
constructor(private readonly log: ToolingLog) {}
|
||||
|
||||
/**
|
||||
* Render an ejs template to a string
|
||||
*/
|
||||
async toString(templatePath: string, destPath: string, vars: Vars) {
|
||||
const context: RenderContext = {
|
||||
...vars,
|
||||
|
||||
// helpers
|
||||
json: this.jsonHelper,
|
||||
js: this.jsHelper,
|
||||
relativePathTo: (rootRelativePath: string) =>
|
||||
normalizePath(
|
||||
Path.relative(Path.dirname(destPath), Path.resolve(REPO_ROOT, rootRelativePath))
|
||||
),
|
||||
};
|
||||
|
||||
this.log.debug('Rendering', templatePath, 'with context', context);
|
||||
const content = await Ejs.renderFile(templatePath, context);
|
||||
return Path.basename(destPath) === 'package.json' ? sortPackageJson(content) : content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an ejs template to a file
|
||||
*/
|
||||
async toFile(templatePath: string, destPath: string, vars: Vars) {
|
||||
const content = await this.toString(templatePath, destPath, vars);
|
||||
this.log.debug('Writing to', destPath);
|
||||
return await Fsp.writeFile(destPath, content);
|
||||
}
|
||||
}
|
15
packages/kbn-generate/src/paths.ts
Normal file
15
packages/kbn-generate/src/paths.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import Path from 'path';
|
||||
|
||||
import { REPO_ROOT } from '@kbn/utils';
|
||||
|
||||
export const ROOT_PKG_DIR = Path.resolve(REPO_ROOT, 'packages');
|
||||
export const TEMPLATE_DIR = Path.resolve(__dirname, '../templates');
|
||||
export const PKG_TEMPLATE_DIR = Path.resolve(TEMPLATE_DIR, 'package');
|
Loading…
Add table
Add a link
Reference in a new issue