Merge pull request #171 from spotify/eide/cli-atomic
[cli]: Use temporary folder for plugin creation to be able to clean up easy
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"@types/html-webpack-plugin": "^3.2.2",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/node": "^13.7.2",
|
||||
"@types/ora": "^3.2.0",
|
||||
"@types/react-dev-utils": "^9.0.4",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/webpack": "^4.41.7",
|
||||
@@ -38,6 +39,7 @@
|
||||
"handlebars": "^4.7.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"inquirer": "^7.0.4",
|
||||
"ora": "^4.0.3",
|
||||
"react-dev-utils": "^10.2.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"replace-in-file": "^5.0.2",
|
||||
|
||||
@@ -5,29 +5,32 @@ import del from 'del';
|
||||
import {
|
||||
createFileFromTemplate,
|
||||
createFromTemplateDir,
|
||||
createPluginFolder,
|
||||
createTemporaryPluginFolder,
|
||||
movePlugin,
|
||||
} from './createPlugin';
|
||||
|
||||
describe('createPlugin', () => {
|
||||
describe('createPluginFolder', () => {
|
||||
it('should create a plugin directory in the correct place', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
||||
it('should create a temporary plugin directory in the correct place', () => {
|
||||
const id = 'testPlugin';
|
||||
const tempDir = path.join(os.tmpdir(), id);
|
||||
try {
|
||||
const pluginFolder = createPluginFolder(tempDir, 'foo');
|
||||
expect(fs.existsSync(pluginFolder)).toBe(true);
|
||||
expect(pluginFolder).toMatch(/packages\/plugins\/foo/);
|
||||
createTemporaryPluginFolder(tempDir);
|
||||
expect(fs.existsSync(tempDir)).toBe(true);
|
||||
expect(tempDir).toMatch(id);
|
||||
} finally {
|
||||
del.sync(tempDir, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should not create a plugin directory if it already exists', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
||||
it('should not create a temporary plugin directory if it already exists', () => {
|
||||
const id = 'testPlugin';
|
||||
const tempDir = path.join(os.tmpdir(), id);
|
||||
try {
|
||||
const pluginFolder = createPluginFolder(tempDir, 'foo');
|
||||
expect(fs.existsSync(pluginFolder)).toBe(true);
|
||||
expect(() => createPluginFolder(tempDir, 'foo')).toThrowError(
|
||||
/A plugin with the same name already exists/,
|
||||
createTemporaryPluginFolder(tempDir);
|
||||
expect(fs.existsSync(tempDir)).toBe(true);
|
||||
expect(() => createTemporaryPluginFolder(tempDir)).toThrowError(
|
||||
/Failed to create temporary plugin directory/,
|
||||
);
|
||||
} finally {
|
||||
del.sync(tempDir, { force: true });
|
||||
@@ -83,4 +86,22 @@ describe('createPlugin', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('movePlugin', () => {
|
||||
it('should move the temporary plugin directory to its final place', () => {
|
||||
const id = 'testPlugin';
|
||||
const tempDir = path.join(os.tmpdir(), id);
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
||||
const pluginDir = path.join(rootDir, 'packages', 'plugins', id);
|
||||
try {
|
||||
createTemporaryPluginFolder(tempDir);
|
||||
movePlugin(tempDir, pluginDir, id);
|
||||
expect(fs.existsSync(pluginDir)).toBe(true);
|
||||
expect(pluginDir).toMatch(`/packages\/plugins\/${id}`);
|
||||
} finally {
|
||||
del.sync(tempDir, { force: true });
|
||||
del.sync(rootDir, { force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,42 +8,49 @@ import { promisify } from 'util';
|
||||
import { exec } from 'child_process';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { realpathSync, existsSync } from 'fs';
|
||||
import os from 'os';
|
||||
import ora from 'ora';
|
||||
|
||||
const MARKER_SUCCESS = chalk.green(` ✓\n`);
|
||||
const MARKER_FAILURE = chalk.red(` ✗\n`);
|
||||
const MARKER_SUCCESS = chalk.green(` ✔︎\n`);
|
||||
const MARKER_FAILURE = chalk.red(` ✘\n`);
|
||||
|
||||
export const createPluginFolder = (rootDir: string, id: string): string => {
|
||||
const checkExists = (rootDir: string, id: string) => {
|
||||
console.log();
|
||||
console.log(chalk.green(' Creating the plugin directory:'));
|
||||
console.log(chalk.green(' Checking if the plugin already exists:'));
|
||||
|
||||
const destination = path.join(rootDir, 'packages', 'plugins', id);
|
||||
|
||||
if (fs.existsSync(destination)) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
` failed:\t ✗ ${chalk.cyan(destination.replace(`${rootDir}/`, ''))}`,
|
||||
` plugin ID\t${chalk.cyan(id)} already exists${MARKER_FAILURE}`,
|
||||
),
|
||||
);
|
||||
throw new Error(
|
||||
`A plugin with the same name already exists: ${chalk.cyan(
|
||||
destination.replace(`${rootDir}/`, ''),
|
||||
)}\nPlease try again with a different Plugin ID`,
|
||||
)}\nPlease try again with a different plugin ID`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
chalk.green(` plugin ID\t${chalk.cyan(id)} is available${MARKER_SUCCESS}`),
|
||||
);
|
||||
};
|
||||
|
||||
export const createTemporaryPluginFolder = (tempDir: string) => {
|
||||
console.log();
|
||||
console.log(chalk.green(' Creating a temporary plugin directory:'));
|
||||
|
||||
process.stdout.write(
|
||||
chalk.green(
|
||||
` creating\t${chalk.cyan(destination.replace(`${rootDir}/`, ''))}`,
|
||||
),
|
||||
chalk.green(` creating\t${chalk.cyan('temporary')} directory`),
|
||||
);
|
||||
try {
|
||||
fs.mkdirSync(destination, { recursive: true });
|
||||
process.stdout.write(chalk.green(' ✓\n'));
|
||||
return destination;
|
||||
fs.mkdirSync(tempDir);
|
||||
process.stdout.write(MARKER_SUCCESS);
|
||||
} catch (e) {
|
||||
process.stdout.write(chalk.red(` ✗\n`));
|
||||
process.stdout.write(MARKER_FAILURE);
|
||||
throw new Error(
|
||||
`Failed to create plugin directory: ${destination}: ${e.message}`,
|
||||
`Failed to create temporary plugin directory: ${e.message}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -129,7 +136,7 @@ export const addPluginDependencyToApp = (
|
||||
'utf-8',
|
||||
);
|
||||
} catch (e) {
|
||||
process.stdout.write(chalk.red(` ✗\n`));
|
||||
process.stdout.write(MARKER_FAILURE);
|
||||
throw new Error(
|
||||
`Failed to add plugin as dependency in app: ${packageFile}: ${e.message}`,
|
||||
);
|
||||
@@ -164,7 +171,7 @@ export const addPluginToApp = (rootDir: string, pluginName: string) => {
|
||||
try {
|
||||
addExportStatement(pluginsFile, pluginExport);
|
||||
} catch (e) {
|
||||
process.stdout.write(chalk.red(` ✗\n`));
|
||||
process.stdout.write(MARKER_FAILURE);
|
||||
throw new Error(
|
||||
`Failed to import plugin in app: ${pluginsFile}: ${e.message}`,
|
||||
);
|
||||
@@ -183,14 +190,17 @@ export const createFromTemplateDir = async (
|
||||
|
||||
let files = [];
|
||||
|
||||
process.stdout.write(chalk.green(` reading\t`));
|
||||
try {
|
||||
files = await recursive(templateFolder);
|
||||
process.stdout.write(
|
||||
chalk.green(`${chalk.cyan(`${files.length} files`)} ✓\n`),
|
||||
console.log(
|
||||
chalk.green(
|
||||
` reading\t${chalk.cyan(`${files.length} files`)}${MARKER_SUCCESS}`,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(chalk.red(` ✗ 0 files\n`));
|
||||
console.log(
|
||||
chalk.red(` reading\t${chalk.cyan('0')} files${MARKER_FAILURE}`),
|
||||
);
|
||||
throw new Error(`Failed to read files in template directory: ${e.message}`);
|
||||
}
|
||||
|
||||
@@ -228,42 +238,26 @@ export const createFromTemplateDir = async (
|
||||
});
|
||||
};
|
||||
|
||||
const cleanUp = async (rootDir: string, id: string) => {
|
||||
const destination = path.join(rootDir, 'packages', 'plugins', id);
|
||||
|
||||
const questions: Question[] = [
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'cleanup',
|
||||
message: chalk.yellow(
|
||||
`It seems that something went wrong when creating the plugin 🤔\nDo you want to remove the following directory and all the files in it:\n${chalk.cyan(
|
||||
destination,
|
||||
)}`,
|
||||
),
|
||||
},
|
||||
];
|
||||
const answers: Answers = await inquirer.prompt(questions);
|
||||
|
||||
if (answers.cleanup) {
|
||||
console.log();
|
||||
console.log(chalk.green(`🧹 Cleaning up...`));
|
||||
console.log();
|
||||
console.log(chalk.green(` Removing plugin:`));
|
||||
process.stdout.write(
|
||||
chalk.green(
|
||||
` deleting\t${chalk.cyan(destination.replace(`${rootDir}/`, ''))}`,
|
||||
),
|
||||
);
|
||||
try {
|
||||
// Not using recursion here, so only empty directories can be removed
|
||||
fs.rmdirSync(destination);
|
||||
process.stdout.write(MARKER_SUCCESS);
|
||||
console.log();
|
||||
} catch (e) {
|
||||
process.stdout.write(MARKER_FAILURE);
|
||||
console.log();
|
||||
console.log(chalk.red(`Failed to cleanup: ${e.message}`));
|
||||
}
|
||||
const cleanUp = async (tempDir: string, id: string) => {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`It seems that something went wrong when creating the plugin 🤔 `,
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.green('We are going to clean up, and then you can try again.'),
|
||||
);
|
||||
const spinner = ora({
|
||||
prefixText: chalk.green(` Cleaning up\t${chalk.cyan(id)}`),
|
||||
spinner: 'arc',
|
||||
color: 'green',
|
||||
}).start();
|
||||
try {
|
||||
await fs.remove(tempDir);
|
||||
spinner.succeed();
|
||||
} catch (e) {
|
||||
spinner.fail();
|
||||
console.log(chalk.red(`Failed to cleanup: ${e.message}`));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -275,13 +269,17 @@ const buildPlugin = async (pluginFolder: string) => {
|
||||
|
||||
const commands = ['yarn install', 'yarn build'];
|
||||
for (const command of commands) {
|
||||
process.stdout.write(chalk.green(` executing\t${chalk.cyan(command)}`));
|
||||
const spinner = ora({
|
||||
prefixText: chalk.green(` executing\t${chalk.cyan(command)}`),
|
||||
spinner: 'arc',
|
||||
color: 'green',
|
||||
}).start();
|
||||
try {
|
||||
process.chdir(pluginFolder);
|
||||
await prom_exec(command, { timeout: 60000 });
|
||||
process.stdout.write(MARKER_SUCCESS);
|
||||
spinner.succeed();
|
||||
} catch (e) {
|
||||
process.stdout.write(MARKER_FAILURE);
|
||||
spinner.fail();
|
||||
throw new Error(
|
||||
`Could not execute command ${chalk.cyan(command)}: ${e.message}`,
|
||||
);
|
||||
@@ -289,7 +287,27 @@ const buildPlugin = async (pluginFolder: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createPlugin = async (): Promise<any> => {
|
||||
export const movePlugin = (
|
||||
tempDir: string,
|
||||
destination: string,
|
||||
id: string,
|
||||
) => {
|
||||
console.log();
|
||||
console.log(chalk.green(` Moving the plugin:`));
|
||||
|
||||
process.stdout.write(
|
||||
chalk.green(` moving\t${chalk.cyan(id)} to final location`),
|
||||
);
|
||||
try {
|
||||
fs.moveSync(tempDir, destination);
|
||||
process.stdout.write(MARKER_SUCCESS);
|
||||
} catch (e) {
|
||||
process.stdout.write(MARKER_FAILURE);
|
||||
throw new Error(`Failed to move plugin: ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const createPlugin = async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
type: 'input',
|
||||
@@ -305,14 +323,25 @@ const createPlugin = async (): Promise<any> => {
|
||||
const appPackage = resolvePath(rootDir, 'packages', 'app');
|
||||
const cliPackage = resolvePath(__dirname, '..', '..');
|
||||
const templateFolder = resolvePath(cliPackage, 'templates', 'default-plugin');
|
||||
const tempDir = path.join(os.tmpdir(), answers.id);
|
||||
const pluginDir = path.join(
|
||||
rootDir,
|
||||
'..',
|
||||
'..',
|
||||
'packages',
|
||||
'plugins',
|
||||
answers.id,
|
||||
);
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green('Creating the plugin...'));
|
||||
|
||||
try {
|
||||
console.log();
|
||||
console.log(chalk.green('🧩 Creating the plugin...'));
|
||||
|
||||
const destinationFolder = createPluginFolder(rootDir, answers.id);
|
||||
await createFromTemplateDir(templateFolder, destinationFolder, answers);
|
||||
await buildPlugin(destinationFolder);
|
||||
checkExists(rootDir, answers.id);
|
||||
createTemporaryPluginFolder(tempDir);
|
||||
await createFromTemplateDir(templateFolder, tempDir, answers);
|
||||
movePlugin(tempDir, pluginDir, answers.id);
|
||||
await buildPlugin(pluginDir);
|
||||
|
||||
if (existsSync(appPackage)) {
|
||||
addPluginDependencyToApp(rootDir, answers.id);
|
||||
@@ -328,16 +357,14 @@ const createPlugin = async (): Promise<any> => {
|
||||
),
|
||||
);
|
||||
console.log();
|
||||
|
||||
return destinationFolder;
|
||||
} catch (e) {
|
||||
console.log();
|
||||
console.log(`${chalk.red(e.message)}`);
|
||||
console.log();
|
||||
await cleanUp(tempDir, answers.id);
|
||||
console.log();
|
||||
console.log(`🔥 ${chalk.red('Failed to create plugin!')}`);
|
||||
console.log();
|
||||
|
||||
await cleanUp(rootDir, answers.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2308,6 +2308,17 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
|
||||
"@material-ui/lab@4.0.0-alpha.45":
|
||||
version "4.0.0-alpha.45"
|
||||
resolved "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.45.tgz#6e1abbdd6e44b9ef7b3eff8ef892a3da5dc52f10"
|
||||
integrity sha512-zT6kUU87SHsPukiu3tlWg8V6o0tGS38c1b/xst/kPqX6eLbfqrROyxhHn1A8ZtHmqga1AKQdv/1llQoG80Afww==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
"@material-ui/utils" "^4.7.1"
|
||||
clsx "^1.0.4"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.0"
|
||||
|
||||
"@material-ui/styles@^4.9.0":
|
||||
version "4.9.0"
|
||||
resolved "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.0.tgz#10c31859f6868cfa9d3adf6b6c3e32c9d676bc76"
|
||||
@@ -3176,6 +3187,13 @@
|
||||
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
|
||||
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
|
||||
|
||||
"@types/ora@^3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/@types/ora/-/ora-3.2.0.tgz#b2f65d1283a8f36d8b0f9ee767e0732a2f429362"
|
||||
integrity sha512-jll99xUKpiFbIFZSQcxm4numfsLaOWBzWNaRk3PvTSE7BPqTzzOCFmS0mQ7m8qkTfmYhuYbehTGsxkvRLPC++w==
|
||||
dependencies:
|
||||
ora "*"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
@@ -4966,6 +4984,11 @@ cli-cursor@^3.1.0:
|
||||
dependencies:
|
||||
restore-cursor "^3.1.0"
|
||||
|
||||
cli-spinners@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77"
|
||||
integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==
|
||||
|
||||
cli-table3@^0.5.0, cli-table3@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
|
||||
@@ -5057,7 +5080,7 @@ clone@^1.0.2:
|
||||
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
|
||||
|
||||
clsx@^1.0.2:
|
||||
clsx@^1.0.2, clsx@^1.0.4:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
|
||||
integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
|
||||
@@ -8979,6 +9002,11 @@ is-installed-globally@^0.1.0:
|
||||
global-dirs "^0.1.0"
|
||||
is-path-inside "^1.0.0"
|
||||
|
||||
is-interactive@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
|
||||
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
|
||||
|
||||
is-npm@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
|
||||
@@ -12299,6 +12327,20 @@ optionator@^0.8.1, optionator@^0.8.3:
|
||||
type-check "~0.3.2"
|
||||
word-wrap "~1.2.3"
|
||||
|
||||
ora@*, ora@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/ora/-/ora-4.0.3.tgz#752a1b7b4be4825546a7a3d59256fa523b6b6d05"
|
||||
integrity sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg==
|
||||
dependencies:
|
||||
chalk "^3.0.0"
|
||||
cli-cursor "^3.1.0"
|
||||
cli-spinners "^2.2.0"
|
||||
is-interactive "^1.0.0"
|
||||
log-symbols "^3.0.0"
|
||||
mute-stream "0.0.8"
|
||||
strip-ansi "^6.0.0"
|
||||
wcwidth "^1.0.1"
|
||||
|
||||
original@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
|
||||
@@ -16748,7 +16790,7 @@ wbuf@^1.1.0, wbuf@^1.7.3:
|
||||
dependencies:
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
wcwidth@^1.0.0:
|
||||
wcwidth@^1.0.0, wcwidth@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
||||
integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
|
||||
|
||||
Reference in New Issue
Block a user