import child_process from 'child_process'; import fs from 'fs-extra'; import watch from 'node-watch'; import path from 'path'; import yargs from 'yargs'; import tar from 'tar'; import { logger } from '@squeebot/core/lib/core'; import { NPMExecutor } from '@squeebot/core/lib/npm'; import { IEnvironment } from '@squeebot/core/lib/types'; import { loadEnvironment } from '@squeebot/core/lib/core'; import { PluginMetaLoader } from '@squeebot/core/lib/plugin'; const tsConfig = { compilerOptions: { downlevelIteration: true, esModuleInterop: true, experimentalDecorators: true, forceConsistentCasingInFileNames: true, skipLibCheck: true, sourceMap: false, strict: true, target: 'es5', }, }; const gitignore = `/node_modules/ /.out/ deployment.json`; function dummyEnvironment(location: string): IEnvironment { return { configurationPath: path.join(location, 'plugins'), environment: 'development', path: location, pluginsPath: path.join(location, 'plugins'), repositoryPath: path.join(location, 'plugins'), }; } function execute(cmd: any[], loc: string): Promise { return new Promise((resolve, reject) => { const cproc = child_process.spawn(cmd[0], cmd.slice(1), { cwd: loc, stdio: 'pipe', }); cproc.on('exit', (code: number) => { if (code !== 0) { return reject(new Error('Build failed!')); } resolve(); }); }); } async function newEnvironment( name?: string, location?: string): Promise { if (!name) { name = 'squeebot'; } if (!location) { location = name; } location = path.resolve(process.cwd(), location); const envFile = path.join(location, name + '.json'); await fs.ensureDir(location); await fs.writeJson(envFile, { environment: 'production', path: location, }); console.log('\nNew environment file:\t', envFile); console.log('Environment path:\t', location); } async function newRepository( name: string, location?: string, typescript = true): Promise { if (!name) { throw new Error('Repository needs a name!'); } if (!location) { location = name; } location = path.resolve(process.cwd(), location); console.log('Creating a new repository development environment at', location); await fs.ensureDir(location); const env = dummyEnvironment(location); console.log('Creating package.json and installing base requirements'); const executor = new NPMExecutor(env, '/home/evert/Squeebot/core'); await executor.loadPackageFile(); let gitIgnore = gitignore + ''; if (typescript) { console.log('Installing TypeScript support'); await executor.installPackage('typescript'); await fs.writeJson(path.join(location, 'tsconfig.json'), tsConfig); gitIgnore += '\n*.js'; gitIgnore += '\n*.d.ts'; gitIgnore += '\n*.tsbuildinfo'; console.log('Adding TypeScript scripts to package.json'); const pkgjson = path.join(location, 'package.json'); const contents = await fs.readJson(pkgjson); contents.scripts = { build: 'tsc', watch: 'tsc -w', }; await fs.writeJson(pkgjson, contents, { spaces: 2 }); } console.log('Creating .gitignore'); await fs.writeFile(path.join(location, '.gitignore'), gitIgnore); console.log('Writing build metadata'); await fs.writeJson(path.join(location, 'squeebot.repo.json'), { name, plugins: [], typescript, }, { spaces: 2 }); console.log('\nDone! Your repository "%s" lives at:', name, location); } async function deploy( name: string, location: string, outDir: string, deployment: string): Promise { const deployFile = path.join(location, 'deployment.json'); if (!await fs.pathExists(deployFile)) { throw new Error('deployment.json file is missing! Can\'t deploy!'); } const meta = await fs.readJson(deployFile); // If we have a path to a metadata file, we copy the output directory to // the plugins directory, overwriting existing plugins with the same name! if ((deployment.indexOf('dev') === 0 || deployment.indexOf('squee') === 0) && meta.devSqueebot) { console.log('Deploying to a configured Squeebot development environment'); const devbotPath = path.resolve(location, meta.devSqueebot); console.log('Path:', devbotPath); if (!await fs.pathExists(devbotPath)) { throw new Error('Development Squeebot environment file doesn\'t exist!'); } const devEnv = await loadEnvironment(devbotPath); if (!devEnv.pluginsPath) { throw new Error('Bad development Squeebot environment file!'); } const pluginsPath = path.resolve(devEnv.path, devEnv.pluginsPath); console.log('Copying plugins to', pluginsPath); const listAllFiles = await fs.readdir(outDir); for (const f of listAllFiles) { if (f === 'repository' || f.indexOf('.tgz') !== -1) { continue; } const dst = path.join(pluginsPath, f); await fs.copy(path.join(outDir, f), dst, { overwrite: true, }); } console.log('Done!'); } } async function buildRepository( location?: string, out = true, doDeploy?: string, onlyDeploy = false): Promise { if (!location) { location = process.cwd(); } location = path.resolve(process.cwd(), location); const outDir = path.join(location, '.out'); const buildMetaFile = path.join(location, 'squeebot.repo.json'); if (!await fs.pathExists(buildMetaFile)) { throw new Error(`${location} is not a valid squeebot repository development environment!`); } const meta = await fs.readJson(buildMetaFile); const env = dummyEnvironment(location); env.pluginsPath = location; if (!meta.name) { throw new Error(`${location} is not a valid squeebot repository development environment!`); } console.log('Detected repository "%s"!', meta.name); if (onlyDeploy) { if (!await fs.pathExists(outDir)) { throw new Error(`You need to build before deploying!`); } else { return deploy(meta.name, location, outDir, doDeploy as string); } } console.log('Detecting plugins in this environment..'); const loader = new PluginMetaLoader(env); const plugins = await loader.loadAll(false); const savedList: any[] = []; console.log('Found the following plugins:', plugins.map((plugin) => { return `${plugin.name}@${plugin.version}`; }).join(', ')); plugins.forEach((plugin) => { savedList.push({ name: plugin.name, version: plugin.version, }); }); meta.plugins = savedList; await fs.writeJson(buildMetaFile, meta, { spaces: 2 }); if (meta.typescript) { console.log('Running build task..'); await execute(['npm', 'run', 'build'], location); } if (!out) { console.log('Done!'); return; } console.log('Creating repository index'); await fs.remove(outDir); await fs.ensureDir(outDir); await fs.writeJSON(path.join(outDir, 'repository.json'), { created: Math.floor(Date.now() / 1000), name: meta.name, plugins: meta.plugins, schema: 1, }); console.log('Copying plugins'); for (const plugin of plugins) { const src = path.join(location, plugin.name); const dst = path.join(outDir, plugin.name); await fs.copy(src, dst); } if (meta.typescript) { console.log('Stripping source files'); for (const plugin of plugins) { const plOut = path.join(outDir, plugin.name); const listAllFiles = await fs.readdir(plOut); for (const f of listAllFiles) { if (f.match(/(\.d)?\.ts$/i) != null) { await fs.remove(path.join(plOut, f)); } } } } console.log('Creating tarballs'); for (const plugin of plugins) { const plOut = path.join(outDir, plugin.name); await tar.c({ gzip: true, file: plOut + '.plugin.tgz', C: outDir, }, [plugin.name]); } if (doDeploy == null) { console.log('Done!'); return; } console.log('\n [!!!] DEPLOYING REPOSITORY [!!!]\n'); deploy(meta.name, location, outDir, doDeploy); } async function watchRepository( location?: string, out = true, doDeploy?: string, onlyDeploy = false): Promise { if (!location) { location = process.cwd(); } const buildMetaFile = path.join(location, 'squeebot.repo.json'); if (!await fs.pathExists(buildMetaFile)) { throw new Error(`${location} is not a valid squeebot repository development environment!`); } const meta = await fs.readJson(buildMetaFile); if (!meta.name) { throw new Error(`${location} is not a valid squeebot repository development environment!`); } console.log('Watching repository "%s"!', meta.name); watch(location, { recursive: true, filter(f, skip): any { if (/\/node_modules/.test(f) || /\.out/.test(f) || /\.git/.test(f) || /\.json$/.test(f)) { return skip; } if (meta.typescript && /\.js$/.test(f)) { return skip; } return true; }, }, (evt, f) => { console.log(`\n ==> ${f} changed, rebuilding...`); buildRepository(location, out, doDeploy, onlyDeploy).catch((e) => { console.error(e); }); }); } const yar = yargs.scriptName('squeebot') .command('new [name] [path]', 'create a new Squeebot environment', (y) => { y.positional('name', { describe: 'The name of the new environment', }) .positional('path', { describe: 'The path to create a new Squeebot environment at (default: working directory)', }); }, (v) => newEnvironment(v.name as string, v.path as string)) .command('repository', 'manage repositories', (y) => { y.command('new [name] [path]', 'create a new repository', (yi) => { yi.positional('name', { demandOption: 'The repository requires a name', describe: 'The name of the new repository', }) .positional('path', { describe: 'The path to create the new Squeebot plugin repository at (default: working directory)', }) .option('t', { alias: 'no-typescript', describe: 'Do not include typescript in the development environment', type: 'boolean', }); }, (v) => newRepository(v.name as string, v.path as string, v.t !== true)); y.command('build [path]', 'build a repository of plugins and generate the index file', (yi) => { yi.positional('path', { describe: 'The path of the repository', }) .option('p', { alias: 'no-output', describe: 'Do not create an output directory, just build', type: 'boolean', }) .option('d', { alias: 'deploy', describe: 'Deploy the output directory as configured', nargs: 1, type: 'string', }) .option('w', { alias: 'watch', describe: 'Watch files for changes', type: 'boolean', }) .option('o', { alias: 'deploy-only', describe: 'Deploy only, without rebuilding', type: 'boolean', }); }, (v) => { const dargs = [ v.path, v.p !== true, v.d, v.o === true, ]; if (v.w) { return watchRepository( dargs[0] as string, dargs[1] as boolean, dargs[2] as string, dargs[3] as boolean, ); } buildRepository( dargs[0] as string, dargs[1] as boolean, dargs[2] as string, dargs[3] as boolean, ); }); }) .argv;