Skip to content

Commit

Permalink
feat: installed as the system command and supports hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
blurooochen committed May 18, 2020
1 parent bf3a34a commit f0970b2
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 111 deletions.
1 change: 1 addition & 0 deletions packages/feflow-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"inquire": "^0.4.8",
"inquirer": "^6.5.0",
"js-yaml": "^3.13.1",
"lookpath": "^1.0.6",
"minimist": "^1.2.0",
"osenv": "^0.1.5",
"package-json": "^6.5.0",
Expand Down
38 changes: 24 additions & 14 deletions packages/feflow-cli/src/core/linker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,42 @@ export default class Linker {
this.currentOs = os.platform();
}

register(binPath: string, libPath: string, name: string, command: string) {
register(binPath: string, libPath: string, command: string) {
if (this.currentOs === 'win32') {
this.linkToWin32(binPath, name, command);
this.linkToWin32(binPath, command);
} else {
this.linkToUnixLike(binPath, libPath, name, command);
this.linkToUnixLike(binPath, libPath, command);
}
}

private linkToWin32(binPath: string, name: string, command: string) {
const file = this.cmdFile(binPath, name);
private linkToWin32(binPath: string, command: string) {
const file = this.cmdFile(binPath, command);
const template = this.cmdTemplate(command);
this.writeExecFile(file, template);
}

private linkToUnixLike(binPath: string, libPath: string, name: string, command: string) {
const file = this.shellFile(libPath, name);
private linkToUnixLike(binPath: string, libPath: string, command: string) {
this.enableLibPath(libPath);
const file = this.shellFile(libPath, command);
const template = this.shellTemplate(command);
const commandLink = path.join(binPath, name);
const commandLink = path.join(binPath, command);
this.writeExecFile(file, template);
if (fs.existsSync(commandLink) && fs.statSync(commandLink).isSymbolicLink) {
return
}
fs.symlinkSync(file, commandLink);
}

private enableLibPath(path: string) {
if (fs.existsSync(path) && fs.statSync(path).isFile()) {
fs.unlinkSync(path);
}

if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
}

private writeExecFile(file: string, content: string) {
const exists = fs.existsSync(file);
if (exists) {
Expand All @@ -54,15 +68,11 @@ export default class Linker {
}

private shellTemplate(command: string): string {
return `#!/bin/sh
fef ${command} "$@"
`;
return `#!/bin/sh\nfef ${command} $@`;
}

private cmdTemplate(command: string): string {
return `@echo off
fef ${command} %*
`;
return `@echo off\nfef ${command} %*`;
}

private shellFile(libPath: string, name: string): string {
Expand Down
63 changes: 47 additions & 16 deletions packages/feflow-cli/src/core/native/install.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import {
getRegistryUrl,
install
} from '../../shared/npm';
import { getRegistryUrl, install } from '../../shared/npm';
import execa from 'execa';
import fs from 'fs';
import path from 'path';
import packageJson from '../../shared/packageJson';
import { parseYaml } from '../../shared/yaml';
import {
UNIVERSAL_MODULES,
UNIVERSAL_PKG_JSON
UNIVERSAL_PKG_JSON,
UNIVERSAL_PLUGIN_CONFIG,
} from '../../shared/constant';
import packageJson from '../../shared/packageJson';
import { Plugin } from '../schema/plugin';
import Linker from '../linker';

async function download(url: string, filepath: string): Promise<any> {
function download(url: string, filepath: string): Promise<any> {
return execa('git', ['clone', url, filepath], {
stdio: 'inherit'
});
}

async function writeDependencies(plugin: string, universalPkgJsonPath: string) {
function writeDependencies(plugin: string, universalPkgJsonPath: string) {
if (!fs.existsSync(universalPkgJsonPath)) {
fs.writeFileSync(universalPkgJsonPath, JSON.stringify({
'name': 'universal-home',
Expand All @@ -31,6 +32,21 @@ async function writeDependencies(plugin: string, universalPkgJsonPath: string) {
fs.writeFileSync(universalPkgJsonPath, JSON.stringify(universalPkgJson, null, 4));
}

function resolvePlugin(ctx: any, repoPath: string): Plugin {
const pluginFile = path.join(repoPath, UNIVERSAL_PLUGIN_CONFIG);
const exists = fs.existsSync(pluginFile);
if (!exists) {
throw `the ${UNIVERSAL_PLUGIN_CONFIG} file was not found`;
}
let config;
try {
config = parseYaml(pluginFile);
} catch(e) {
throw `the ${UNIVERSAL_PLUGIN_CONFIG} file failed to resolve, please check the syntax, e: ${e}`;
}
return new Plugin(ctx, repoPath, config)
}

module.exports = (ctx: any) => {
const packageManager = ctx.config && ctx.config.packageManager;
const universalModules = path.join(ctx.root, UNIVERSAL_MODULES);
Expand All @@ -43,19 +59,34 @@ module.exports = (ctx: any) => {
if (/(.git)/.test(dependencies[0])) {
const repoUrl = dependencies[0];
const match = repoUrl.match(/\/([a-zA-Z0-9]*).git$/);
let repoName = match && match[1];
ctx.logger.debug(`Repo name is: ${ repoName }`);
if (!/^feflow-plugin/.test(repoName)) {
repoName = `feflow-plugin-${ repoName }`;
const command = match && match[1];
let repoName: string;
ctx.logger.debug(`Repo name is: ${ command }`);
if (/^feflow-plugin/.test(command)) {
repoName = command;
} else {
repoName = `feflow-plugin-${ command }`;
}
const repoPath = path.join(universalModules, repoName);
if (!fs.existsSync(repoPath)) {
ctx.logger.info(`Start download from ${ repoUrl }`);
await download(repoUrl, repoPath);
ctx.logger.debug('Write package to universal-package.json');
await writeDependencies(repoName, universalPkgJsonPath);
ctx.logger.info('install success');
}
}
ctx.logger.debug('Write package to universal-package.json');

const plugin = resolvePlugin(ctx, repoPath);
// check the validity of the plugin before installing it
await plugin.check();
ctx.logger.debug('check plugin success');

plugin.preInstall.run();
new Linker().register(ctx.bin, ctx.lib, command);

writeDependencies(repoName, universalPkgJsonPath);
plugin.test.run();
plugin.postInstall.run();

ctx.logger.info('install success');
} else {
await Promise.all(
dependencies.map((dependency: string) => {
Expand Down
100 changes: 19 additions & 81 deletions packages/feflow-cli/src/core/plugin/loadUniversalPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import fs from 'fs';
import path from 'path';
import spawn from 'cross-spawn';
import os from 'os';
import { parseYaml } from '../../shared/yaml';
import { UNIVERSAL_MODULES, UNIVERSAL_PKG_JSON, UNIVERSAL_PLUGIN_CONFIG } from '../../shared/constant';
import { Plugin } from '../schema/plugin';
import {
UNIVERSAL_MODULES,
UNIVERSAL_PKG_JSON,
UNIVERSAL_PLUGIN_CONFIG
} from '../../shared/constant';

type PluginCommandMap = {
default?: string;
windows?: string;
linux?: string;
mac?: string;
};

type PluginPkgConfig = {
dependencies: object;
Expand All @@ -20,46 +17,6 @@ type PluginPkgConfig = {

const toolRegex = /^feflow-(?:devkit|plugin)-(.*)/i;

const platformMap = {
aix: 'linux',
freebsd: 'linux',
linux: 'linux',
openbsd: 'linux',
sunos: 'linux',
win32: 'windows',
darwin: 'mac',
};

const platform = platformMap[os.platform()];

// environment variables in universal plugin command
// var:td => universal plugin absolute path
const envVarsAnchors = [/\${var:td}/gi];
const envVars = [] as any;

const SPACES_REGEXP = / +/g;

// Allow spaces to be escaped by a backslash if not meant as a delimiter
const handleEscaping = (tokens: string[], token: string, index: number) => {
if (index === 0) {
return [token];
}

const previousToken = tokens[tokens.length - 1];

if (previousToken.endsWith('\\')) {
return [...tokens.slice(0, -1), `${previousToken.slice(0, -1)} ${token}`];
}

return [...tokens, token];
};

const parseCommand = (command: string) => {
return command
.trim()
.split(SPACES_REGEXP)
.reduce(handleEscaping, []);
};

export default function loadUniversalPlugin(ctx: any): Promise<any> {
const { root, logger } = ctx;
Expand All @@ -71,7 +28,7 @@ export default function loadUniversalPlugin(ctx: any): Promise<any> {
}

return new Promise(resolve => {
fs.readFile(pluginPkg, 'utf8', (err, data) => {
fs.readFile(pluginPkg, 'utf8', async (err, data) => {
if (err) {
logger.debug(err);
resolve();
Expand All @@ -87,10 +44,9 @@ export default function loadUniversalPlugin(ctx: any): Promise<any> {

// traverse universal plugins and register command
const { dependencies = {} } = pluginPkgConfig;
Object.keys(dependencies).forEach(pluginName => {
for (const pluginName of Object.keys(dependencies)) {
const pluginPath = path.resolve(root, UNIVERSAL_MODULES, pluginName);
const pluginConfigPath = path.resolve(pluginPath, UNIVERSAL_PLUGIN_CONFIG);
envVars.push(pluginPath);

// get universal plugin command, like fef [universal-plugin-command]
const pluginCommand = (toolRegex.exec(pluginName) || [])[1];
Expand All @@ -101,40 +57,22 @@ export default function loadUniversalPlugin(ctx: any): Promise<any> {

if (fs.existsSync(pluginConfigPath)) {
const config = parseYaml(pluginConfigPath) || {};
const { command = {}, description } = config;
const commandMap = {} as PluginCommandMap;
const supportPlatform = Object.keys(command);
if (!supportPlatform.length) {
return logger.debug(`there is no default command in ${pluginName}`);
}
// parse universal plugin command form it's config ,
// it provides kinds of command which dependencies user os platform .
// repalce env variable
supportPlatform.forEach(platform => {
commandMap[platform] = envVarsAnchors.reduce((previousValue, currentEnvVar, index) => {
return previousValue.replace(currentEnvVar, envVars[index] || '');
}, command[platform]);
});
const plugin = new Plugin(ctx, pluginPath, config);
await plugin.check()

const pluginDescriptions = description || `${pluginCommand} universal plugin description`;
const pluginDescriptions = plugin.desc || `${pluginCommand} universal plugin description`;

ctx.commander.register(pluginCommand, pluginDescriptions, () => {
const argGroup: string[] = [];
const nativeArgs = process.argv.slice(3);
const commandStrFromConfig = commandMap[platform] || commandMap.default;
if (!commandStrFromConfig) {
return logger.error(`universal plugin ${pluginCommand} is not supported on ${platform}`);
}
const [command, ...commandArgs] = parseCommand(commandStrFromConfig);
argGroup.push(...commandArgs);
argGroup.push(...nativeArgs);
logger.debug('command: ', command);
logger.debug('argGroup: ', argGroup);
// run universal plugin
spawn(command, argGroup, { stdio: 'inherit' });
plugin.preRun.run();
const args = process.argv.slice(3);
plugin.command.run(args, (out: string, err?: any): boolean => {
out && console.log(out);
return err ? false : true;
});
plugin.postRun.run();
});
}
});
}

resolve();
});
Expand Down
30 changes: 30 additions & 0 deletions packages/feflow-cli/src/core/schema/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os from 'os';

const platformMap = {
aix: 'linux',
freebsd: 'linux',
linux: 'linux',
openbsd: 'linux',
sunos: 'linux',
win32: 'windows',
darwin: 'macos',
};

const platform = os.platform();
const platformType = platformMap[platform];

function toArray(v: any, field: string, defaultV?: string[]): string[] {
if (v && !Array.isArray(v)) {
if (typeof v === 'string') {
return [v];
}
throw `field ${field} must provide either a string or an array of strings`;
}
return v || defaultV || [];
}

export {
platform,
platformType,
toArray,
};
Loading

0 comments on commit f0970b2

Please sign in to comment.