This commit is contained in:
Fredrik Adelöw
2020-02-19 14:39:20 +01:00
parent 5b5ec6e98d
commit 1b76e07910
14 changed files with 394 additions and 13 deletions
+1
View File
@@ -2,3 +2,4 @@ secrets.env
.DS_Store
cjs/
esm/
types/
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../cjs');
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env node
const hello = require('./src/index.ts');
console.log('output:', hello());
+15 -3
View File
@@ -8,12 +8,24 @@
"scripts": {
"build": "web-scripts build",
"lint": "web-scripts lint",
"test": "web-scripts test"
"test": "web-scripts test",
"start": "nodemon ."
},
"devDependencies": {
"@spotify/web-scripts": "^6.0.0"
"@spotify/web-scripts": "^6.0.0",
"@types/node": "^13.7.2",
"nodemon": "^2.0.2",
"ts-node": "^8.6.2"
},
"bin": {
"backstage-cli": "cli.js"
"backstage-cli": "bin/backstage-cli"
},
"dependencies": {
"commander": "^4.1.1"
},
"nodemonConfig": {
"watch": "./src",
"exec": "ts-node",
"ext": "ts"
}
}
@@ -0,0 +1,187 @@
const path = require('path');
const fs = require('fs');
const fsExtra = require('fs-extra');
const replace = require('replace-in-file');
const dashify = require('dashify');
const inquirer = require('inquirer');
const handlebars = require('handlebars');
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const lowercaseFirstLetter = str => str.charAt(0).toLowerCase() + str.slice(1);
const appendTextFile = (file, textToAppend) => {
const originalContents = fs.readFileSync(file, 'utf8');
// Make sure that there is a newline before and after the new text
if (originalContents && !originalContents.endsWith('\n')) {
textToAppend = `\n${textToAppend}`;
}
if (textToAppend && !textToAppend.endsWith('\n')) {
textToAppend = `${textToAppend}\n`;
}
fs.appendFileSync(file, textToAppend);
};
const questions = [
{
type: 'input',
name: 'id',
message:
"Enter an ID for the plugin (Please capitalize) e.g. 'MyAwesomePlugin' [required]",
validate: value => (value ? true : 'Please enter an ID for the plugin'),
},
{
type: 'confirm',
name: 'ts',
message: 'Use Typescript?',
},
{
type: 'input',
name: 'owner',
message: 'Which squad will own this plugin? [required]',
validate: value =>
value ? true : 'Please enter a squad that will own the plugin',
},
{
type: 'input',
name: 'title',
message: "Enter a title for the plugin e.g. 'My Awesome Plugin' [optional]",
default: '',
},
{
type: 'input',
name: 'desc',
message:
"Enter a description for the plugin e.g. 'This Plugin does all awesome things!' [optional]",
default: '',
},
{
type: 'input',
name: 'author',
message: "Author's slack handle e.g. alund [optional]",
default: '',
},
{
type: 'input',
name: 'support_channel',
message:
'Slack channel to contact for support on this plugin e.g. data-support [optional]',
default: '',
},
];
const createPluginCommand = () => {
inquirer.prompt(questions).then(answers => {
const pluginId = capitalize(answers.id);
const pluginFolderName = lowercaseFirstLetter(
pluginId.replace(/Plugin/g, ''),
);
answers.folder = pluginFolderName;
const pluginRoute = `/${dashify(pluginFolderName)}`;
answers.route = pluginRoute;
const pluginRoot = path.join(__dirname, '../src/plugins/');
const source = path.join(pluginRoot, 'scaffold');
const destination = path.join(pluginRoot, pluginFolderName);
const pluginImportText = `import { ${pluginId} } from 'plugins/${pluginFolderName}';\n// @@import`;
const pluginDefinitionText = `${pluginId},\n // @@definition`;
const pluginManagerBootstrap = path.join(
pluginRoot,
'pluginManagerBootstrap.js',
);
if (fs.existsSync(destination)) {
return console.error(
`Uh Oh! Looks like another plugin exists with the same name.\nPlease check ${destination} directory.`,
);
}
try {
fs.mkdirSync(destination);
const compileFileAndWrite = (sourceFile, destFile) => {
const hbFile = fs.readFileSync(path.join(source, sourceFile));
const template = handlebars.compile(hbFile.toString());
const contents = template({ name: destFile, ...answers });
fs.writeFile(path.join(destination, destFile), contents, err => {
if (err) {
return console.log(
'Error writing file ',
path.join(destination, destFile),
);
}
console.log('Wrote ', path.join(destination, destFile));
});
};
['Page', 'Plugin'].forEach(fileType => {
compileFileAndWrite(
`Scaffold${fileType}.hbs`,
`${pluginId}${fileType}${answers.ts ? '.tsx' : '.js'}`,
);
});
compileFileAndWrite('ScaffoldPage.test.hbs', `${pluginId}Page.test.js`);
compileFileAndWrite('plugin-info.hbs', 'plugin-info.yaml');
compileFileAndWrite('index.hbs', 'index.js');
appendTextFile(
'.github/CODEOWNERS',
`/src/plugins/${pluginFolderName}/ @backstage/${answers.owner}`,
);
// import and add plugin to bootstrap
const options = {
files: pluginManagerBootstrap,
from: [/\/\/ @@import/g, /\/\/ @@definition/],
to: [pluginImportText, pluginDefinitionText],
};
replace(options, err => {
if (err) {
return console.error(err);
}
console.log('');
console.log(
'Added plugin to bootstrap, and the plugin directory to CODEOWNERS.',
);
console.log(
`You are all set! Check http://localhost:5678${pluginRoute}`,
);
console.log('');
console.log(
'NOTE: First thing to do now should be to create a Pull Request and wait for the',
);
console.log(
'current goalie of the Tools squad to review and merge it. After that is done,',
);
console.log(
'through the use of the CODEOWNERS feature, you should be able to continue',
);
console.log('development inside your plugin directory on your own.');
console.log('');
console.log('Hack away!');
});
} catch (e) {
console.error(e);
fsExtra.removeSync(destination);
const options = {
files: pluginManagerBootstrap,
from: [pluginImportText, pluginImportText],
to: [/\/\/ @@import/g, /\/\/ @@definition/],
};
replace(options, err => {
if (err) {
return console.error(err);
}
console.log('Cleaned up');
});
}
});
};
export default createPluginCommand;
+21 -2
View File
@@ -1,3 +1,22 @@
const hello = () => 'Hello World';
import program from 'commander';
import createPluginCommand from './commands/createPlugin';
module.exports = hello;
const main = (argv: string[]) => {
program
.command('create-plugin')
.description('Creates a new plugin in the current repository')
.action(createPluginCommand);
program.on('command:*', () => {
console.error(
'Invalid command: %s\nSee --help for a list of available commands.',
program.args.join(' '),
);
process.exit(1);
});
program.parse(argv);
};
// main(process.argv);
main([process.argv[0], process.argv[1], 'create-plugin', '--bla']);
@@ -0,0 +1,32 @@
import React{{#if ts}}, { FC }{{/if}} from 'react';
import { Typography } from '@material-ui/core';
import { theme } from 'core/app/PageThemeProvider';
import InfoCard from 'shared/components/InfoCard';
import { Content, ContentHeader, Header, HeaderLabel, Navigation, NavItem, Page } from 'shared/components/layout';
/**
* This component demonstrates how to render a page with its own navigation.
* You typically create layouts that can be shared between multiple pages.
* Layouts can be found in {@link src/shared/components/layout/}
*/
const {{id}}Page{{#if ts}}: FC<>{{/if}} = () => {
return (
<Page theme={theme.tool}>
<Header type="Tool" title="{{id}}">
<HeaderLabel label="Owner" value="{{owner}}" url="/org/{{owner}}" />
<HeaderLabel label="Lifecycle" value="Experimental" />
</Header>
<Navigation>
<NavItem title="Overview" href="{{route}}" />
</Navigation>
<Content>
<ContentHeader title="{{id}} Title" />
<InfoCard>
<Typography>Hello world, this is your new plugin!</Typography>
</InfoCard>
</Content>
</Page>
);
};
export default {{id}}Page;
@@ -0,0 +1,14 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from 'testUtils';
import {{id}}Page from './{{id}}Page';
const minProps = {};
describe('<{{id}}Page />', () => {
it('renders without exploding', () => {
const { getByText } = render(wrapInThemedTestApp(<{{id}}Page {...minProps} />));
expect(getByText(/Hello world.*/)).toBeInTheDocument();
});
});
@@ -0,0 +1,14 @@
import React from 'react';
import { createPlugin } from 'shared/pluginApi';
const {{id}}Page = React.lazy(
/* istanbul ignore next */ () => import(/* webpackChunkName: "{{folder}}" */ 'plugins/{{folder}}/{{id}}Page'),
);
export default createPlugin({
manifest: require('./plugin-info.yaml'),
register({ router }) {
router.registerRoute('{{route}}', {{id}}Page);
}
});
@@ -0,0 +1 @@
export { default as {{id}} } from 'plugins/{{folder}}/{{id}}Plugin';
@@ -0,0 +1 @@
export { default as ScaffoldPlugin } from 'plugins/scaffold/ScaffoldPlugin';
@@ -0,0 +1,10 @@
---
id: {{id}}
title: {{title}}
description: {{desc}}
owner: {{owner}}
visibility: public
facts:
support_channel: {{support_channel}}
authors:
- slack: {{author}}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@spotify/web-scripts/config/tsconfig.json",
"include": ["src"],
"compilerOptions": {
"paths": {
"*": ["src/*"]
}
}
}
+87 -4
View File
@@ -2920,6 +2920,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-12.12.26.tgz#213e153babac0ed169d44a6d919501e68f59dea9"
integrity sha512-UmUm94/QZvU5xLcUlNR8hA7Ac+fGpO1EG/a8bcWVz0P0LqtxFmun9Y2bbtuckwGboWJIT70DoWq1r3hb56n3DA==
"@types/node@^13.7.2":
version "13.7.2"
resolved "https://registry.npmjs.org/@types/node/-/node-13.7.2.tgz#50375b95b5845a34efda2ffb3a087c7becbc46c6"
integrity sha512-uvilvAQbdJvnSBFcKJ2td4016urcGvsiR+N4dHGU87ml8O2Vl6l+ErOi9w0kXSPiwJ1AYlIW+0pDXDWWMOiWbw==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
@@ -3510,6 +3515,11 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -4506,7 +4516,7 @@ chokidar@^2.0.2, chokidar@^2.1.8:
optionalDependencies:
fsevents "^1.2.7"
chokidar@^3.3.0:
chokidar@^3.2.2, chokidar@^3.3.0:
version "3.3.1"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
@@ -4823,7 +4833,7 @@ commander@^2.11.0, commander@^2.20.0, commander@~2.20.3:
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^4.0.1:
commander@^4.0.1, commander@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
@@ -5676,7 +5686,7 @@ debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5:
debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@@ -5884,6 +5894,11 @@ diff-sequences@^25.1.0:
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32"
integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -8082,6 +8097,11 @@ iferr@^1.0.2:
resolved "https://registry.npmjs.org/iferr/-/iferr-1.0.2.tgz#e9fde49a9da06dc4a4194c6c9ed6d08305037a6d"
integrity sha512-9AfeLfji44r5TKInjhz3W9DyZI1zR1JAf2hVBMGhddAKPqBsupb89jGfbCTHIGZd6fGZl9WlHdn4AObygyMKwg==
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk=
ignore-walk@^3.0.1:
version "3.0.3"
resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
@@ -10611,6 +10631,11 @@ make-error@1.x:
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
make-fetch-happen@^5.0.0:
version "5.0.2"
resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz#aa8387104f2687edca01c8687ee45013d02d19bd"
@@ -11328,6 +11353,22 @@ node-releases@^1.1.47:
dependencies:
semver "^6.3.0"
nodemon@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.2.tgz#9c7efeaaf9b8259295a97e5d4585ba8f0cbe50b0"
integrity sha512-GWhYPMfde2+M0FsHnggIHXTqPDHXia32HRhh6H0d75Mt9FKUoCBvumNHr7LdrpPBTKxsWmIEOjoN+P4IU6Hcaw==
dependencies:
chokidar "^3.2.2"
debug "^3.2.6"
ignore-by-default "^1.0.1"
minimatch "^3.0.4"
pstree.remy "^1.1.7"
semver "^5.7.1"
supports-color "^5.5.0"
touch "^3.1.0"
undefsafe "^2.0.2"
update-notifier "^2.5.0"
nopt@^4.0.1, nopt@~4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
@@ -11336,6 +11377,13 @@ nopt@^4.0.1, nopt@~4.0.1:
abbrev "1"
osenv "^0.1.4"
nopt@~1.0.10:
version "1.0.10"
resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=
dependencies:
abbrev "1"
normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -13273,6 +13321,11 @@ psl@^1.1.24, psl@^1.1.28:
resolved "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
pstree.remy@^1.1.7:
version "1.1.7"
resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3"
integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==
public-encrypt@^4.0.0:
version "4.0.3"
resolved "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@@ -15337,7 +15390,7 @@ supports-color@^2.0.0:
resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
supports-color@^5.3.0:
supports-color@^5.3.0, supports-color@^5.5.0:
version "5.5.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@@ -15686,6 +15739,13 @@ toidentifier@1.0.0:
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
dependencies:
nopt "~1.0.10"
tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.5.0:
version "2.5.0"
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -15759,6 +15819,17 @@ ts-jest@^25.0.0:
semver "^5.5"
yargs-parser "10.x"
ts-node@^8.6.2:
version "8.6.2"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35"
integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.6"
yn "3.1.1"
ts-pnp@1.1.5, ts-pnp@^1.1.2:
version "1.1.5"
resolved "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.5.tgz#840e0739c89fce5f3abd9037bb091dbff16d9dec"
@@ -15886,6 +15957,13 @@ umask@^1.1.0, umask@~1.1.0:
resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
undefsafe@^2.0.2:
version "2.0.3"
resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae"
integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==
dependencies:
debug "^2.2.0"
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -16936,6 +17014,11 @@ yargs@^8.0.2:
y18n "^3.2.1"
yargs-parser "^7.0.0"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
zen-observable@^0.8.15:
version "0.8.15"
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"