refactor: CLI

This commit is contained in:
cha0s 2022-03-01 01:52:56 -06:00
parent 73f36ab3c0
commit 54d46ec04c
20 changed files with 349 additions and 163 deletions

View File

@ -65,6 +65,7 @@
"eslint": "^7.0.0",
"eslint-import-resolver-webpack": "0.13.0",
"js-yaml": "3.14.0",
"jsonparse": "^1.3.1",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.2",
"lodash.intersection": "^4.4.0",

View File

@ -4,6 +4,7 @@ import {join, resolve, sep} from 'path';
import {Command} from 'commander';
import D from './debug';
import {processCode} from './server/commands';
import Flecks from './server/flecks';
const {
@ -63,17 +64,16 @@ else {
process.exitCode = child;
return;
}
const reject = (error) => {
try {
const code = await processCode(child);
debug('action exited with code %d', code);
process.exitCode = code;
}
catch (error) {
// eslint-disable-next-line no-console
console.error(error);
process.exitCode = child.exitCode || 1;
};
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
debug('action exited with code %d', code);
process.exitCode = code;
});
}
};
// Initialize Commander.
const program = new Command();

View File

@ -14,12 +14,23 @@ const {
const debug = D('@flecks/core/commands');
const flecksRoot = normalize(FLECKS_CORE_ROOT);
export const spawnWith = (cmd, localEnv, spawnArgs) => {
debug('spawning:\n%s %s\nwith local environment: %O', cmd, spawnArgs.join(' '), localEnv);
const spawnOptions = {
env: {...localEnv, ...process.env},
};
const child = spawn('npx', [cmd, ...spawnArgs], spawnOptions);
export const processCode = (child) => new Promise((resolve, reject) => {
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
resolve(code);
});
});
export const spawnWith = (cmd, opts = {}) => {
debug("spawning: '%s' with options: %O", cmd.join(' '), opts);
const child = spawn(cmd[0], cmd.slice(1), {
...opts,
env: {
...process.env,
...(opts.env || {}),
},
});
child.stderr.pipe(process.stderr);
child.stdout.pipe(process.stdout);
return child;
@ -97,19 +108,24 @@ export default (program, flecks) => {
} = opts;
debug('Building...', opts);
const webpackConfig = flecks.localConfig('webpack.config.js', '@flecks/core');
const localEnv = {
...targetNeutrinos(flecks),
...(target ? {FLECKS_CORE_BUILD_LIST: target} : {}),
...(hot ? {FLECKS_ENV_FLECKS_SERVER_hot: 'true'} : {}),
};
const spawnArgs = [
const cmd = [
'npx', 'webpack',
'--colors',
'--config', webpackConfig,
'--mode', (production && !hot) ? 'production' : 'development',
...(verbose ? ['--stats', 'verbose'] : []),
...((watch || hot) ? ['--watch'] : []),
];
return spawnWith('webpack', localEnv, spawnArgs);
return spawnWith(
cmd,
{
env: {
...targetNeutrinos(flecks),
...(target ? {FLECKS_CORE_BUILD_LIST: target} : {}),
...(hot ? {FLECKS_ENV_FLECKS_SERVER_hot: 'true'} : {}),
},
},
);
},
};
commands.lint = {
@ -126,7 +142,8 @@ export default (program, flecks) => {
continue;
}
process.env.FLECKS_CORE_BUILD_TARGET = target;
const spawnArgs = [
const cmd = [
'npx', 'eslint',
'--config', flecks.localConfig(
`${target}.eslintrc.js`,
'@flecks/core',
@ -136,12 +153,16 @@ export default (program, flecks) => {
'--ext', 'js',
'.',
];
const localEnv = {
FLECKS_CORE_BUILD_TARGET: target,
...targetNeutrinos(flecks),
};
promises.push(new Promise((resolve, reject) => {
const child = spawnWith('eslint', localEnv, spawnArgs);
const child = spawnWith(
cmd,
{
env: {
FLECKS_CORE_BUILD_TARGET: target,
...targetNeutrinos(flecks),
},
},
);
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);

View File

@ -6,6 +6,7 @@ import R from '../bootstrap/require';
export {
default as commands,
processCode,
spawnWith,
targetNeutrino,
targetNeutrinos,
@ -13,6 +14,7 @@ export {
export {default as Flecks} from './flecks';
export {default as require} from '../bootstrap/require';
export {JsonStream, transform} from './stream';
export default {
[Hooks]: {

View File

@ -0,0 +1,57 @@
// eslint-disable-next-line max-classes-per-file
import JsonParse from 'jsonparse';
import {Transform} from 'stream';
export class JsonStream extends Transform {
constructor() {
super();
const self = this;
this.done = undefined;
this.parser = new JsonParse();
this.parser.onValue = function onValue(O) {
if (0 === this.stack.length) {
self.push(JSON.stringify(O));
self.done();
}
};
}
// eslint-disable-next-line no-underscore-dangle
_transform(chunk, encoding, done) {
this.done = done;
this.parser.write(chunk);
}
}
JsonStream.PrettyPrint = class extends Transform {
constructor(indent = 2) {
super();
this.indent = indent;
}
// eslint-disable-next-line no-underscore-dangle
async _transform(chunk, encoding, done) {
this.push(JSON.stringify(JSON.parse(chunk), null, this.indent));
done();
}
};
export const transform = (fn, opts = {}) => {
class EasyTransform extends Transform {
constructor() {
super(opts);
}
// eslint-disable-next-line no-underscore-dangle, class-methods-use-this
_transform(chunk, encoding, done) {
fn(chunk, encoding, done, this);
}
}
return new EasyTransform();
};

View File

@ -22,12 +22,16 @@
"files": [
"cli.js",
"cli.js.map",
"server.js",
"server.js.map",
"src",
"template"
],
"dependencies": {
"@flecks/core": "^1.1.1",
"fs-extra": "10.0.0"
"glob": "^7.2.0",
"minimatch": "^5.0.1",
"validate-npm-package-name": "^3.0.0"
},
"devDependencies": {
"@flecks/fleck": "^1.1.1"

View File

@ -0,0 +1,9 @@
import {processCode, spawnWith} from '@flecks/core/server';
export default async (cwd) => {
const code = await processCode(spawnWith(['yarn'], {cwd}));
if (0 !== code) {
return code;
}
return processCode(spawnWith(['yarn', 'build'], {cwd}));
};

View File

@ -1,50 +1,36 @@
import {spawn} from 'child_process';
import {readFileSync, writeFileSync} from 'fs';
import {join, normalize} from 'path';
import {
copySync,
mkdirpSync,
moveSync,
} from 'fs-extra';
import {Flecks} from '@flecks/core/server';
import validate from 'validate-npm-package-name';
const cwd = normalize(process.cwd());
import build from './build';
import move from './move';
const forwardProcessCode = (fn) => async (...args) => {
process.exitCode = await fn(args.slice(0, -2));
};
const {
FLECKS_CORE_ROOT = process.cwd(),
} = process.env;
const processCode = (child) => new Promise((resolve, reject) => {
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
resolve(code);
});
});
const cwd = normalize(FLECKS_CORE_ROOT);
const create = () => async () => {
const create = async (flecks) => {
const name = process.argv[2];
const path = name.split('/').pop();
copySync(join(__dirname, 'template'), join(cwd, path), {recursive: true});
mkdirpSync(join(cwd, path, 'packages'));
moveSync(join(cwd, path, '.gitignore.extraneous'), join(cwd, path, '.gitignore'));
moveSync(join(cwd, path, 'package.json.extraneous'), join(cwd, path, 'package.json'));
writeFileSync(
join(cwd, path, 'package.json'),
JSON.stringify(
{
name: `@${name}/monorepo`,
...JSON.parse(readFileSync(join(cwd, path, 'package.json')).toString()),
},
null,
2,
),
);
const code = await processCode(spawn('yarn', [], {cwd: join(cwd, path), stdio: 'inherit'}));
if (0 !== code) {
return code;
const {errors} = validate(name);
if (errors) {
throw new Error(`@flecks/create-app: invalid app name: ${errors.join(', ')}`);
}
return processCode(spawn('yarn', ['build'], {cwd: join(cwd, path), stdio: 'inherit'}));
const destination = join(cwd, name);
await move(name, join(__dirname, 'template'), destination, flecks);
await build(destination);
};
forwardProcessCode(create())();
(async () => {
const flecks = await Flecks.bootstrap();
try {
await create(flecks);
}
catch (error) {
// eslint-disable-next-line no-console
console.error(error.message);
process.exitCode = 1;
}
})();

View File

@ -0,0 +1,58 @@
import {
stat,
} from 'fs/promises';
import {basename} from 'path';
import {JsonStream, transform} from '@flecks/core/server';
import FileTree from './tree';
const testDestination = async (destination) => {
try {
await stat(destination);
return false;
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return true;
}
};
export default async (name, source, destination, flecks) => {
if (!await testDestination(destination)) {
const error = new Error(
`@flecks/create-fleck: destination '${destination} already exists: aborting`,
);
error.code = 129;
throw error;
}
const fileTree = await FileTree.loadFrom(source);
// Renamed to avoid conflicts.
const {files} = fileTree;
fileTree.glob('**/*.noconflict')
.forEach((path) => {
files[basename(path, '.noconflict')] = files[path];
delete files[path];
});
// Defaults.
flecks.set('@flecks/create-fleck.packager', flecks.get('@flecks/create-fleck.packager', ['...']));
// Send it out.
await flecks.invokeSequentialAsync('@flecks/create-fleck/packager', fileTree);
// Add project name to `package.json`.
fileTree.pipe(
'package.json',
transform((chunk, encoding, done, stream) => {
stream.push(JSON.stringify({name, ...JSON.parse(chunk)}));
done();
}),
);
// Pretty print all JSON.
fileTree.glob('**/*.json')
.forEach((path) => {
fileTree.pipe(path, new JsonStream.PrettyPrint());
});
// Write the tree.
await fileTree.writeTo(destination);
};

View File

@ -0,0 +1,5 @@
export {default as validate} from 'validate-npm-package-name';
export {default as build} from './build';
export {default as move} from './move';
export {default as FileTree} from './tree';

View File

@ -0,0 +1,72 @@
import {createReadStream, createWriteStream} from 'fs';
import {mkdir, stat} from 'fs/promises';
import glob from 'glob';
import minimatch from 'minimatch';
import {dirname, join} from 'path';
export default class FileTree {
constructor(files = {}) {
this.files = files;
}
addDirectory(path) {
this.files[path] = null;
}
addFile(path, stream) {
this.files[path] = stream;
}
glob(glob) {
return Object.keys(this.files).filter((path) => minimatch(path, glob, {dot: true}));
}
static async loadFrom(cwd) {
const paths = await new Promise((r, e) => {
glob(
'**/*',
{cwd, dot: true},
(error, paths) => (error ? e(error) : r(paths)),
);
});
return new FileTree(
await paths
.reduce(
async (r, path) => {
const origin = join(cwd, path);
const stats = await stat(origin);
return {
...await r,
[path]: stats.isDirectory() ? null : createReadStream(origin),
};
},
{},
),
);
}
pipe(path, stream) {
this.files[path] = this.files[path] ? this.files[path].pipe(stream) : undefined;
}
async writeTo(destination) {
return Promise.all(
Object.entries(this.files)
.map(async ([path, stream]) => {
if (null === stream) {
return mkdir(path, {recursive: true});
}
await mkdir(dirname(join(destination, path)), {recursive: true});
return new Promise((resolve, reject) => {
const writer = createWriteStream(join(destination, path));
writer.on('finish', resolve);
writer.on('error', reject);
stream.pipe(writer);
});
}),
);
}
}

View File

@ -1,7 +1,4 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
@ -14,11 +11,11 @@
"resolveSourceMapLocations": [],
"sourceMapPathOverrides": {
"webpack:///*": "${workspaceFolder}/*",
"webpack://*": "node_modules/*",
"webpack://*": "node_modules/*"
},
"cwd": "${workspaceFolder}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start"],
"runtimeArgs": ["run", "start"]
}
]
}

View File

@ -2,7 +2,10 @@
"private": true,
"scripts": {
"build": "flecks build",
"dev": "npm run -- build -h"
"debug": "DEBUG=*,-babel* npm run dev",
"dev": "npm run -- build -h",
"repl": "npx flecks repl --rlwrap",
"start": "DEBUG=@flecks*,-@flecks/core/flecks* npm run dev",
},
"dependencies": {
"@flecks/core": "^1.0.0",

View File

@ -27,8 +27,7 @@
],
"dependencies": {
"@flecks/core": "^1.1.1",
"fs-extra": "10.0.0",
"validate-npm-package-name": "^3.0.0"
"@flecks/create-app": "^1.1.1"
},
"devDependencies": {
"@flecks/fleck": "^1.1.1"

View File

@ -1,13 +1,8 @@
import {spawn} from 'child_process';
import {
readFileSync,
statSync,
writeFileSync,
} from 'fs';
import {stat} from 'fs/promises';
import {join, normalize} from 'path';
import {copySync, moveSync} from 'fs-extra';
import validate from 'validate-npm-package-name';
import {build, move, validate} from '@flecks/create-app/server';
import {Flecks} from '@flecks/core/server';
const {
FLECKS_CORE_ROOT = process.cwd(),
@ -15,97 +10,68 @@ const {
const cwd = normalize(FLECKS_CORE_ROOT);
const forwardProcessCode = (fn) => async (...args) => {
process.exitCode = await fn(args.slice(0, -2));
const hasPackages = async (cwd) => {
try {
await stat(join(cwd, 'packages'));
return true;
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return false;
}
};
const processCode = (child) => new Promise((resolve, reject) => {
child.on('error', reject);
child.on('exit', (code) => {
child.off('error', reject);
resolve(code);
});
});
const monorepoScope = () => {
const monorepoScope = async (cwd) => {
try {
statSync(join(cwd, 'packages'));
const {name} = __non_webpack_require__(join(cwd, 'package.json'));
const [scope] = name.split('/');
return scope;
}
catch (error) {
if ('ENOENT' !== error.code) {
if ('MODULE_NOT_FOUND' !== error.code) {
throw error;
}
return undefined;
}
};
const testDestination = (destination) => {
try {
statSync(destination);
return false;
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
return true;
}
};
const create = () => async () => {
const rawname = process.argv[2];
const {errors} = validate(rawname);
const target = async (name) => {
const {errors} = validate(name);
if (errors) {
// eslint-disable-next-line no-console
console.error(`@flecks/create-fleck: invalid fleck name: ${errors.join(', ')}`);
return 128;
throw new Error(`@flecks/create-fleck: invalid fleck name: ${errors.join(', ')}`);
}
const parts = rawname.split('/');
let path = cwd;
const parts = name.split('/');
let pkg;
let scope;
if (1 === parts.length) {
pkg = rawname;
}
else {
[scope, pkg] = parts;
}
if (!scope) {
scope = monorepoScope();
if (scope) {
path = join(path, 'packages');
pkg = name;
if (await hasPackages(cwd)) {
scope = await monorepoScope(cwd);
}
return [scope, pkg];
}
const name = [scope, pkg].filter((e) => !!e).join('/');
const destination = join(path, pkg);
if (!testDestination(destination)) {
// eslint-disable-next-line no-console
console.error(`@flecks/create-fleck: destination '${destination} already exists: aborting`);
return 129;
}
// eslint-disable-next-line no-unreachable
copySync(join(__dirname, 'template'), destination, {recursive: true});
moveSync(join(destination, '.gitignore.extraneous'), join(destination, '.gitignore'));
moveSync(join(destination, 'package.json.extraneous'), join(destination, 'package.json'));
writeFileSync(
join(destination, 'package.json'),
JSON.stringify(
{
name,
...JSON.parse(readFileSync(join(destination, 'package.json')).toString()),
},
null,
2,
),
);
const code = await processCode(spawn('yarn', [], {cwd: destination, stdio: 'inherit'}));
if (0 !== code) {
return code;
}
return processCode(spawn('yarn', ['build'], {cwd: destination, stdio: 'inherit'}));
return parts;
};
forwardProcessCode(create())();
const create = async (flecks) => {
const [scope, pkg] = await target(process.argv[2]);
const path = scope && (await hasPackages(cwd)) ? join(cwd, 'packages') : cwd;
const name = [scope, pkg].filter((e) => !!e).join('/');
const destination = join(path, pkg);
await move(name, join(__dirname, 'template'), destination, flecks);
await build(destination);
};
(async () => {
const flecks = await Flecks.bootstrap();
try {
await create(flecks);
}
catch (error) {
// eslint-disable-next-line no-console
console.error(error.message);
process.exitCode = 1;
}
})();

View File

@ -8,6 +8,7 @@
"test": "flecks test"
},
"files": [
"build",
"index.js",
"index.js.map",
"src",

View File

@ -55,13 +55,13 @@ export default (program, flecks) => {
}
}
const spawnMocha = () => {
const localEnv = {};
const spawnArgs = [
const cmd = [
'npx', 'mocha',
'--colors',
'--reporter', 'min',
testLocation,
];
return spawnWith('mocha', localEnv, spawnArgs);
return spawnWith(cmd);
};
if (!watch) {
await new Promise((resolve, reject) => {

View File

@ -18,15 +18,20 @@ export default {
return;
}
// Otherwise, spawn `webpack-dev-server` (WDS).
const localEnv = {
FLECKS_CORE_BUILD_LIST: 'http',
};
const spawnArgs = [
const cmd = [
'npx', 'webpack-dev-server',
'--mode', 'development',
'--hot',
'--config', flecks.localConfig('webpack.config.js', '@flecks/core'),
];
spawnWith('webpack-dev-server', localEnv, spawnArgs);
spawnWith(
cmd,
{
env: {
FLECKS_CORE_BUILD_LIST: 'http',
},
},
);
// Remove the build config since we're handing off to WDS.
// eslint-disable-next-line no-param-reassign
delete neutrinoConfigs.http;