#!/usr/bin/env node -r esm import { spawn, exec } from 'child_process'; import inquirer from 'inquirer'; import chalk from 'chalk'; import detectFreePort from 'detect-port'; import dedent from 'ts-dedent'; import fs from 'fs'; import nodeCleanup from 'node-cleanup'; const logger = console; const freePort = (port) => detectFreePort(port); let verdaccioProcess; const startVerdaccio = (port) => { let resolved = false; return Promise.race([ new Promise((res) => { verdaccioProcess = spawn('npx', [ 'verdaccio@4.0.1', '-c', 'scripts/verdaccio.yaml', '-l', port, ]); verdaccioProcess.stdout.on('data', (data) => { if (!resolved && data && data.toString().match(/http address/)) { const [url] = data.toString().match(/(http:.*\d\/)/); res(url); resolved = true; } fs.appendFile('verdaccio.log', data, (err) => { if (err) { throw err; } }); }); }), new Promise((res, rej) => { setTimeout(() => { if (!resolved) { rej(new Error(`TIMEOUT - verdaccio didn't start within 60s`)); resolved = true; verdaccioProcess.kill(); } }, 60000); }), ]); }; const registryUrl = (command, url) => new Promise((res, rej) => { const args = url ? ['config', 'set', 'registry', url] : ['config', 'get', 'registry']; exec(`${command} ${args.join(' ')}`, (e, stdout) => { if (e) { rej(e); } else { res(url || stdout.toString().trim()); } }); }); const registriesUrl = (yarnUrl, npmUrl) => Promise.all([registryUrl('yarn', yarnUrl), registryUrl('npm', npmUrl || yarnUrl)]); nodeCleanup(() => { try { verdaccioProcess.kill(); } catch (e) { // } }); const applyRegistriesUrl = (yarnUrl, npmUrl, originalYarnUrl, originalNpmUrl) => { logger.log(`â†Ēī¸ changing system config`); nodeCleanup(() => { registriesUrl(originalYarnUrl, originalNpmUrl); logger.log(dedent` Your registry config has been restored from: npm: ${npmUrl} to ${originalNpmUrl} yarn: ${yarnUrl} to ${originalYarnUrl} `); }); return registriesUrl(yarnUrl, npmUrl); }; const addUser = (url) => new Promise((res, rej) => { logger.log(`👤 add temp user to verdaccio`); exec(`npx npm-cli-adduser -r "${url}" -a -u user -p password -e user@example.com`, (e) => { if (e) { rej(e); } else { res(); } }); }); const currentVersion = async () => { const { version } = (await import('../lerna.json')).default; return version; }; const publish = (packages, url) => packages.reduce((acc, { name, location }) => { return acc.then(() => { return new Promise((res, rej) => { logger.log(`đŸ›Ģ publishing ${name} (${location})`); const command = `cd ${location} && npm publish --registry ${url} --force --access restricted`; exec(command, (e) => { if (e) { rej(e); } else { logger.log(`đŸ›Ŧ successful publish of ${name}!`); res(); } }); }); }); }, Promise.resolve()); const listOfPackages = () => new Promise((res, rej) => { const command = `./node_modules/.bin/lerna list --json`; exec(command, (e, result) => { if (e) { rej(e); } else { const data = JSON.parse(result.toString().trim()); res(data); } }); }); const askForPermission = () => inquirer .prompt([ { type: 'confirm', message: `${chalk.red('BE WARNED')} do you want to change your ${chalk.underline( 'system' )} default registry to the temp verdacio registry?`, name: 'sure', }, ]) .then(({ sure }) => sure); const askForReset = () => inquirer .prompt([ { type: 'confirm', message: `${chalk.red( 'THIS IS BAD' )} looks like something bad happened, OR you're already using a local registry, shall we reset to the default registry https://registry.npmjs.org/ ?`, name: 'sure', }, ]) .then(({ sure }) => { if (sure) { logger.log(`↩ī¸ changing system config`); return registriesUrl('https://registry.npmjs.org/'); } return process.exit(1); }); const askForPublish = (packages, url, version) => inquirer .prompt([ { type: 'confirm', message: `${chalk.green('READY TO PUBLISH')} shall we kick off a publish?`, name: 'sure', }, ]) .then(({ sure }) => { if (sure) { logger.log(`🚀 publishing version ${version}`); return publish(packages, url).then(() => askForPublish(packages, url, version)); } return false; }); const askForSubset = (packages) => inquirer .prompt([ { type: 'checkbox', message: 'which packages?', name: 'subset', pageSize: packages.length, choices: packages.map((p) => ({ name: p.name, checked: true })), }, ]) .then(({ subset }) => packages.filter((p) => subset.includes(p.name))); const run = async () => { const port = await freePort(4873); logger.log(`🌏 found a open port: ${port}`); logger.log(`🔖 reading current registry settings`); let [originalYarnRegistryUrl, originalNpmRegistryUrl] = await registriesUrl(); if ( originalYarnRegistryUrl.includes('localhost') || originalNpmRegistryUrl.includes('localhost') ) { await askForReset(); originalYarnRegistryUrl = 'https://registry.npmjs.org/'; originalNpmRegistryUrl = 'https://registry.npmjs.org/'; } logger.log(`📐 reading version of storybook`); logger.log(`🚛 listing storybook packages`); logger.log(`đŸŽŦ starting verdaccio (this takes Âą20 seconds, so be patient)`); const [shouldOverwrite, verdaccioUrl, packages, version] = await Promise.all([ askForPermission(), startVerdaccio(port), listOfPackages(), currentVersion(), ]); logger.log(`đŸŒŋ verdaccio running on ${verdaccioUrl}`); if (shouldOverwrite) { logger.log(dedent` You have chosen to change your system's default registry url. If this process fails for some reason and doesn't exit correctly, you may be stuck with a npm/yarn config that's broken. To fix this you can revert back to the registry urls you had before by running: > npm config set registry ${originalNpmRegistryUrl} > yarn config set registry ${originalYarnRegistryUrl} You can now use regular install procedure anywhere on your machine and the storybook packages will be installed from this local registry The registry url is: ${verdaccioUrl} `); } else { logger.log(dedent` You have chosen to NOT change your system's default registry url. The registry is running locally, but you'll need to add a npm/yarn config file in your project in that points to the registry. Here's a documentation for npm: https://docs.npmjs.com/files/npmrc Yarn is able to read this file as well The registry url is: ${verdaccioUrl} `); } if (shouldOverwrite) { await applyRegistriesUrl( verdaccioUrl, verdaccioUrl, originalYarnRegistryUrl, originalNpmRegistryUrl ); } await addUser(verdaccioUrl); logger.log(`đŸ“Ļ found ${packages.length} storybook packages at version ${chalk.blue(version)}`); const subset = await askForSubset(packages); await askForPublish(subset, verdaccioUrl, version); logger.log(dedent` The verdaccio registry will now be terminated (this can take Âą15 seconds, please be patient) `); verdaccioProcess.kill(); }; run();