Commit 7ec87ee7 by Ryan McKinley Committed by Dominik Prokop

grafana/toolkit: improve CircleCI stubs (#17995)

* validate type and id

* copy all svg and png, useful if people don't use the img folder

* update comments

* add stubs for each ci task

* use ci-work folder rather than build

* use axios for basic testing

* Packages: publish packages@6.3.0-alpha.39

* bump version

* add download task

* Packages: publish packages@6.3.0-alpha.40

* merge all dist folders into one

* fix folder paths

* Fix ts error

* Packages: publish packages@6.3.0-beta.0

* Packages: publish packages@6.3.0-beta.1

* bump next to 6.4

* Packages: publish packages@6.4.0-alpha.2

* better build and bundle tasks

* fix lint

* Packages: publish packages@6.4.0-alpha.3

* copy the file to start grafana

* Packages: publish packages@6.4.0-alpha.4

* use sudo for copy

* Packages: publish packages@6.4.0-alpha.5

* add missing service

* add service and homepath

* Packages: publish packages@6.4.0-alpha.6

* make the folder

* Update packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts

* Update packages/grafana-toolkit/src/cli/tasks/plugin.ci.ts
parent 51909499
...@@ -2,5 +2,5 @@ ...@@ -2,5 +2,5 @@
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"packages": ["packages/*"], "packages": ["packages/*"],
"version": "6.3.0-alpha.36" "version": "6.4.0-alpha.6"
} }
...@@ -148,7 +148,7 @@ ...@@ -148,7 +148,7 @@
"themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts", "themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
"packages:prepare": "lerna run clean && npm run test && lerna version --tag-version-prefix=\"packages@\" -m \"Packages: publish %s\" --no-push", "packages:prepare": "lerna run clean && npm run test && lerna version --tag-version-prefix=\"packages@\" -m \"Packages: publish %s\" --no-push",
"packages:build": "lerna run clean && lerna run build", "packages:build": "lerna run clean && lerna run build",
"packages:publish": "lerna publish from-package --contents dist --tag-version-prefix=\"packages@\" --dist-tag next" "packages:publish": "lerna publish from-package --contents dist --dist-tag next --tag-version-prefix=\"packages@\""
}, },
"husky": { "husky": {
"hooks": { "hooks": {
......
# Grafana Data Library # Grafana Data Library
The core data components This package holds the root data types and functions used within Grafana.
\ No newline at end of file \ No newline at end of file
{ {
"name": "@grafana/data", "name": "@grafana/data",
"version": "6.3.0-alpha.36", "version": "6.4.0-alpha.2",
"description": "Grafana Data Library", "description": "Grafana Data Library",
"keywords": [ "keywords": [
"typescript" "typescript"
......
# Grafana Runtime library # Grafana Runtime library
Interfaces that let you use the runtime... This package allows access to grafana services. It requires Grafana to be running already and the functions to be imported as externals.
\ No newline at end of file \ No newline at end of file
{ {
"name": "@grafana/runtime", "name": "@grafana/runtime",
"version": "6.3.0-alpha.36", "version": "6.4.0-alpha.2",
"description": "Grafana Runtime Library", "description": "Grafana Runtime Library",
"keywords": [ "keywords": [
"typescript", "grafana"
"react",
"react-component"
], ],
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
......
{ {
"name": "@grafana/toolkit", "name": "@grafana/toolkit",
"version": "6.3.0-alpha.36", "version": "6.4.0-alpha.6",
"description": "Grafana Toolkit", "description": "Grafana Toolkit",
"keywords": [ "keywords": [
"typescript", "grafana",
"react", "cli",
"react-component" "plugins"
], ],
"bin": { "bin": {
"grafana-toolkit": "./bin/grafana-toolkit.js" "grafana-toolkit": "./bin/grafana-toolkit.js"
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
"@types/node": "^12.0.4", "@types/node": "^12.0.4",
"@types/react-dev-utils": "^9.0.1", "@types/react-dev-utils": "^9.0.1",
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/tmp": "^0.1.0",
"@types/webpack": "4.4.34", "@types/webpack": "4.4.34",
"axios": "0.19.0", "axios": "0.19.0",
"babel-loader": "8.0.6", "babel-loader": "8.0.6",
......
...@@ -13,7 +13,13 @@ import { pluginTestTask } from './tasks/plugin.tests'; ...@@ -13,7 +13,13 @@ import { pluginTestTask } from './tasks/plugin.tests';
import { searchTestDataSetupTask } from './tasks/searchTestDataSetup'; import { searchTestDataSetupTask } from './tasks/searchTestDataSetup';
import { closeMilestoneTask } from './tasks/closeMilestone'; import { closeMilestoneTask } from './tasks/closeMilestone';
import { pluginDevTask } from './tasks/plugin.dev'; import { pluginDevTask } from './tasks/plugin.dev';
import { pluginCITask } from './tasks/plugin.ci'; import {
ciBuildPluginTask,
ciBundlePluginTask,
ciTestPluginTask,
ciDeployPluginTask,
ciSetupPluginTask,
} from './tasks/plugin.ci';
import { buildPackageTask } from './tasks/package.build'; import { buildPackageTask } from './tasks/package.build';
export const run = (includeInternalScripts = false) => { export const run = (includeInternalScripts = false) => {
...@@ -141,15 +147,47 @@ export const run = (includeInternalScripts = false) => { ...@@ -141,15 +147,47 @@ export const run = (includeInternalScripts = false) => {
}); });
program program
.command('plugin:ci') .command('plugin:ci-build')
.option('--dryRun', "Dry run (don't post results)") .option('--platform <platform>', 'For backend task, which backend to run')
.description('Run Plugin CI task') .description('Build the plugin, leaving artifacts in /dist')
.action(async cmd => { .action(async cmd => {
await execTask(pluginCITask)({ await execTask(ciBuildPluginTask)({
dryRun: cmd.dryRun, platform: cmd.platform,
}); });
}); });
program
.command('plugin:ci-bundle')
.description('Create a zip artifact for the plugin')
.action(async cmd => {
await execTask(ciBundlePluginTask)({});
});
program
.command('plugin:ci-setup')
.option('--installer <installer>', 'Name of installer to download and run')
.description('Install and configure grafana')
.action(async cmd => {
await execTask(ciSetupPluginTask)({
installer: cmd.installer,
});
});
program
.command('plugin:ci-test')
.description('end-to-end test using bundle in /artifacts')
.action(async cmd => {
await execTask(ciTestPluginTask)({
platform: cmd.platform,
});
});
program
.command('plugin:ci-deploy')
.description('Publish plugin CI results')
.action(async cmd => {
await execTask(ciDeployPluginTask)({});
});
program.on('command:*', () => { program.on('command:*', () => {
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' ')); console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
process.exit(1); process.exit(1);
......
...@@ -9,7 +9,8 @@ import path = require('path'); ...@@ -9,7 +9,8 @@ import path = require('path');
import fs = require('fs'); import fs = require('fs');
export interface PluginCIOptions { export interface PluginCIOptions {
dryRun?: boolean; platform?: string;
installer?: string;
} }
const calcJavascriptSize = (base: string, files?: string[]): number => { const calcJavascriptSize = (base: string, files?: string[]): number => {
...@@ -32,22 +33,164 @@ const calcJavascriptSize = (base: string, files?: string[]): number => { ...@@ -32,22 +33,164 @@ const calcJavascriptSize = (base: string, files?: string[]): number => {
return size; return size;
}; };
const pluginCIRunner: TaskRunner<PluginCIOptions> = async ({ dryRun }) => { const getJobFromProcessArgv = () => {
const arg = process.argv[2];
if (arg && arg.startsWith('plugin:ci-')) {
const task = arg.substring('plugin:ci-'.length);
if ('build' === task) {
if ('--platform' === process.argv[3] && process.argv[4]) {
return task + '_' + process.argv[4];
}
return 'build_nodejs';
}
return task;
}
return 'unknown_job';
};
// /**
// * Like cp -rn... BUT error if an destination file exists
// */
// async function copyDirErrorIfExists(src:string,dest:string) {
// const entries = await fs.readdirSync(src,{withFileTypes:true});
// if(!fs.existsSync(dest)) {
// fs.mkdirSync(dest);
// }
// console.log( 'DIR', src );
// for(let entry of entries) {
// const srcPath = path.join(src,entry.name);
// const destPath = path.join(dest,entry.name);
// if(entry.isDirectory()) {
// await copyDirErrorIfExists(srcPath,destPath);
// } else if(fs.existsSync(destPath)) {
// console.log( 'XXXXXXXXXXXXXXX', destPath );
// console.log( 'XXXXXXXXXXXXXXX', destPath );
// throw new Error('Duplicate entry: '+destPath);
// }
// else {
// // console.log( 'COPY', destPath );
// await fs.copyFileSync(srcPath,destPath);
// }
// }
// }
const job = process.env.CIRCLE_JOB || getJobFromProcessArgv();
const getJobFolder = () => {
const dir = path.resolve(process.cwd(), 'ci', 'jobs', job);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
};
const getCiFolder = () => {
const dir = path.resolve(process.cwd(), 'ci');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
};
const writeJobStats = (startTime: number, workDir: string) => {
const stats = {
job,
startTime,
endTime: Date.now(),
};
const f = path.resolve(workDir, 'stats.json');
fs.writeFile(f, JSON.stringify(stats, null, 2), err => {
if (err) {
throw new Error('Unable to stats: ' + f);
}
});
};
/**
* 1. BUILD
*
* when platform exists it is building backend, otherwise frontend
*
* Each build writes data:
* ~/work/build_xxx/
*
* Anything that should be put into the final zip file should be put in:
* ~/work/build_xxx/dist
*/
const buildPluginRunner: TaskRunner<PluginCIOptions> = async ({ platform }) => {
const start = Date.now(); const start = Date.now();
const distDir = `${process.cwd()}/dist`; const workDir = getJobFolder();
const artifactsDir = `${process.cwd()}/artifacts`; await execa('rimraf', [workDir]);
await execa('rimraf', [`${process.cwd()}/coverage`]); fs.mkdirSync(workDir);
await execa('rimraf', [artifactsDir]);
// Do regular build process if (platform) {
console.log('TODO, backend support?');
fs.mkdirSync(path.resolve(process.cwd(), 'dist'));
const file = path.resolve(process.cwd(), 'dist', `README_${platform}.txt`);
fs.writeFile(file, `TODO... build ${platform}!`, err => {
if (err) {
throw new Error('Unable to write: ' + file);
}
});
} else {
// Do regular build process with coverage
await pluginBuildRunner({ coverage: true }); await pluginBuildRunner({ coverage: true });
const elapsed = Date.now() - start; }
// Move local folders to the scoped job folder
for (const name of ['dist', 'coverage']) {
const dir = path.resolve(process.cwd(), name);
if (fs.existsSync(dir)) {
fs.renameSync(dir, path.resolve(workDir, name));
}
}
writeJobStats(start, workDir);
};
export const ciBuildPluginTask = new Task<PluginCIOptions>('Build Plugin', buildPluginRunner);
if (!fs.existsSync(artifactsDir)) { /**
* 2. BUNDLE
*
* Take everything from `~/ci/job/{any}/dist` and
* 1. merge it into: `~/ci/dist`
* 2. zip it into artifacts in `~/ci/artifacts`
* 3. prepare grafana environment in: `~/ci/grafana-test-env`
*
*/
const bundlePluginRunner: TaskRunner<PluginCIOptions> = async () => {
const start = Date.now();
const ciDir = getCiFolder();
const artifactsDir = path.resolve(ciDir, 'artifacts');
const distDir = path.resolve(ciDir, 'dist');
const grafanaEnvDir = path.resolve(ciDir, 'grafana-test-env');
await execa('rimraf', [artifactsDir, distDir, grafanaEnvDir]);
fs.mkdirSync(artifactsDir); fs.mkdirSync(artifactsDir);
fs.mkdirSync(distDir);
fs.mkdirSync(grafanaEnvDir);
console.log('Build Dist Folder');
// 1. Check for a local 'dist' folder
const d = path.resolve(process.cwd(), 'dist');
if (fs.existsSync(d)) {
await execa('cp', ['-rn', d + '/.', distDir]);
}
// 2. Look for any 'dist' folders under ci/job/XXX/dist
const dirs = fs.readdirSync(path.resolve(ciDir, 'jobs'));
for (const j of dirs) {
const contents = path.resolve(ciDir, 'jobs', j, 'dist');
if (fs.existsSync(contents)) {
try {
await execa('cp', ['-rn', contents + '/.', distDir]);
} catch (er) {
throw new Error('Duplicate files found in dist folders');
}
}
} }
// TODO? can this typed from @grafana/ui? console.log('Building ZIP');
const pluginInfo = getPluginJson(`${distDir}/plugin.json`); const pluginInfo = getPluginJson(`${distDir}/plugin.json`);
const zipName = pluginInfo.id + '-' + pluginInfo.info.version + '.zip'; const zipName = pluginInfo.id + '-' + pluginInfo.info.version + '.zip';
const zipFile = path.resolve(artifactsDir, zipName); const zipFile = path.resolve(artifactsDir, zipName);
...@@ -55,23 +198,184 @@ const pluginCIRunner: TaskRunner<PluginCIOptions> = async ({ dryRun }) => { ...@@ -55,23 +198,184 @@ const pluginCIRunner: TaskRunner<PluginCIOptions> = async ({ dryRun }) => {
await execa('zip', ['-r', zipFile, '.']); await execa('zip', ['-r', zipFile, '.']);
restoreCwd(); restoreCwd();
const zipStats = fs.statSync(zipFile);
if (zipStats.size < 100) {
throw new Error('Invalid zip file: ' + zipFile);
}
let sha1 = undefined;
try {
const exe = await execa('shasum', [zipFile]);
const idx = exe.stdout.indexOf(' ');
sha1 = exe.stdout.substring(0, idx);
fs.writeFile(zipFile + '.sha1', sha1, err => {});
} catch {
console.warn('Unable to read SHA1 Checksum');
}
const info = {
name: zipName,
sha1,
size: zipStats.size,
};
let p = path.resolve(artifactsDir, 'info.json');
fs.writeFile(p, JSON.stringify(info, null, 2), err => {
if (err) {
throw new Error('Error writing artifact info: ' + p);
}
});
console.log('Setup Grafan Environment');
p = path.resolve(grafanaEnvDir, 'plugins', pluginInfo.id);
fs.mkdirSync(p, { recursive: true });
await execa('unzip', [zipFile, '-d', p]);
// Write the custom settings
p = path.resolve(grafanaEnvDir, 'custom.ini');
const customIniBody =
`# Autogenerated by @grafana/toolkit \n` +
`[paths] \n` +
`plugins = ${path.resolve(grafanaEnvDir, 'plugins')}\n` +
`\n`; // empty line
fs.writeFile(p, customIniBody, err => {
if (err) {
throw new Error('Unable to write: ' + p);
}
});
writeJobStats(start, getJobFolder());
};
export const ciBundlePluginTask = new Task<PluginCIOptions>('Bundle Plugin', bundlePluginRunner);
/**
* 3. Setup (install grafana and setup provisioning)
*
* deploy the zip to a running grafana instance
*
*/
const setupPluginRunner: TaskRunner<PluginCIOptions> = async ({ installer }) => {
const start = Date.now();
if (!installer) {
throw new Error('Missing installer path');
}
// Download the grafana installer
const installDir = path.resolve(process.cwd(), '.installer');
const installFile = path.resolve(installDir, installer);
if (!fs.existsSync(installFile)) {
if (!fs.existsSync(installDir)) {
fs.mkdirSync(installDir);
}
console.log('download', installer);
const exe = await execa('wget', ['-O', installFile, 'https://dl.grafana.com/oss/release/' + installer]);
console.log(exe.stdout);
}
console.log('Install Grafana');
let exe = await execa('sudo', ['apt-get', 'install', '-y', 'adduser', 'libfontconfig1']);
exe = await execa('sudo', ['dpkg', '-i', installFile]);
console.log(exe.stdout);
const customIniFile = path.resolve(getCiFolder(), 'grafana-test-env', 'custom.ini');
const configDir = '/usr/share/grafana/conf/';
exe = await execa('sudo', ['cp', '-f', customIniFile, configDir]);
console.log(exe.stdout);
// sudo service grafana-server start
console.log('Starting Grafana');
exe = await execa('sudo', ['service', 'grafana-server', 'start']);
console.log(exe.stdout);
// exe = await execa('grafana-cli', ['--version', '--homepath', '/usr/share/grafana']);
// console.log(exe.stdout);
// exe = await execa('grafana-cli', ['plugins', 'ls', '--homepath', '/usr/share/grafana']);
// console.log(exe.stdout);
const dir = getJobFolder() + '_setup';
await execa('rimraf', [dir]);
fs.mkdirSync(dir);
writeJobStats(start, dir);
};
export const ciSetupPluginTask = new Task<PluginCIOptions>('Setup Grafana', setupPluginRunner);
/**
* 4. Test (end-to-end)
*
* deploy the zip to a running grafana instance
*
*/
const testPluginRunner: TaskRunner<PluginCIOptions> = async ({ platform }) => {
const start = Date.now();
const workDir = getJobFolder();
const args = {
withCredentials: true,
baseURL: process.env.GRAFANA_URL || 'http://localhost:3000/',
responseType: 'json',
auth: {
username: 'admin',
password: 'admin',
},
};
const axios = require('axios');
const frontendSettings = await axios.get('api/frontend/settings', args);
console.log('Grafana Version: ' + JSON.stringify(frontendSettings.data.buildInfo, null, 2));
const pluginInfo = getPluginJson(`${process.cwd()}/src/plugin.json`);
const pluginSettings = await axios.get(`api/plugins/${pluginInfo.id}/settings`, args);
console.log('Plugin Info: ' + JSON.stringify(pluginSettings.data, null, 2));
console.log('TODO puppeteer');
const elapsed = Date.now() - start;
const stats = { const stats = {
job,
sha1: `${process.env.CIRCLE_SHA1}`,
startTime: start, startTime: start,
buildTime: elapsed, buildTime: elapsed,
jsSize: calcJavascriptSize(distDir),
zipSize: fs.statSync(zipFile).size,
endTime: Date.now(), endTime: Date.now(),
}; };
fs.writeFile(artifactsDir + '/stats.json', JSON.stringify(stats, null, 2), err => {
if (err) {
throw new Error('Unable to write stats');
}
console.log('Stats', stats);
});
if (!dryRun) { console.log('TODO Puppeteer Tests', stats);
console.log('TODO send info to github?'); writeJobStats(start, workDir);
};
export const ciTestPluginTask = new Task<PluginCIOptions>('Test Plugin (e2e)', testPluginRunner);
/**
* 4. Deploy
*
* deploy the zip to a running grafana instance
*
*/
const deployPluginRunner: TaskRunner<PluginCIOptions> = async () => {
const start = Date.now();
// TASK Time
if (process.env.CIRCLE_INTERNAL_TASK_DATA) {
const timingInfo = fs.readdirSync(`${process.env.CIRCLE_INTERNAL_TASK_DATA}`);
if (timingInfo) {
timingInfo.forEach(file => {
console.log('TIMING INFO: ', file);
});
} }
}
const elapsed = Date.now() - start;
const stats = {
job,
sha1: `${process.env.CIRCLE_SHA1}`,
startTime: start,
buildTime: elapsed,
endTime: Date.now(),
};
console.log('TODO DEPLOY??', stats);
console.log(' if PR => write a comment to github with difference ');
console.log(' if master | vXYZ ==> upload artifacts to some repo ');
}; };
export const pluginCITask = new Task<PluginCIOptions>('Plugin CI', pluginCIRunner); export const ciDeployPluginTask = new Task<PluginCIOptions>('Deploy plugin', deployPluginRunner);
...@@ -3,7 +3,7 @@ import { getPluginJson, validatePluginJson } from './pluginValidation'; ...@@ -3,7 +3,7 @@ import { getPluginJson, validatePluginJson } from './pluginValidation';
describe('pluginValdation', () => { describe('pluginValdation', () => {
describe('plugin.json', () => { describe('plugin.json', () => {
test('missing plugin.json file', () => { test('missing plugin.json file', () => {
expect(() => getPluginJson(`${__dirname}/mocks/missing-plugin-json`)).toThrow('plugin.json file is missing!'); expect(() => getPluginJson(`${__dirname}/mocks/missing-plugin.json`)).toThrowError();
}); });
}); });
......
import path = require('path');
// See: packages/grafana-ui/src/types/plugin.ts // See: packages/grafana-ui/src/types/plugin.ts
interface PluginJSONSchema { interface PluginJSONSchema {
id: string; id: string;
...@@ -22,15 +20,24 @@ export const validatePluginJson = (pluginJson: any) => { ...@@ -22,15 +20,24 @@ export const validatePluginJson = (pluginJson: any) => {
if (!pluginJson.info.version) { if (!pluginJson.info.version) {
throw new Error('Plugin info.version is missing in plugin.json'); throw new Error('Plugin info.version is missing in plugin.json');
} }
const types = ['panel', 'datasource', 'app'];
const type = pluginJson.type;
if (!types.includes(type)) {
throw new Error('Invalid plugin type in plugin.json: ' + type);
}
if (!pluginJson.id.endsWith('-' + type)) {
throw new Error('[plugin.json] id should end with: -' + type);
}
}; };
export const getPluginJson = (root: string = process.cwd()): PluginJSONSchema => { export const getPluginJson = (path: string): PluginJSONSchema => {
let pluginJson; let pluginJson;
try { try {
pluginJson = require(path.resolve(root, 'src/plugin.json')); pluginJson = require(path);
} catch (e) { } catch (e) {
throw new Error('plugin.json file is missing!'); throw new Error('Unable to find: ' + path);
} }
validatePluginJson(pluginJson); validatePluginJson(pluginJson);
......
{ {
"name": "@grafana/ui", "name": "@grafana/ui",
"version": "6.3.0-alpha.36", "version": "6.4.0-alpha.2",
"description": "Grafana Components Library", "description": "Grafana Components Library",
"keywords": [ "keywords": [
"typescript", "grafana",
"react", "react",
"react-component" "react-component"
], ],
......
...@@ -3371,6 +3371,11 @@ ...@@ -3371,6 +3371,11 @@
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf" resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf"
integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw== integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw==
"@types/tmp@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd"
integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==
"@types/uglify-js@*": "@types/uglify-js@*":
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
...@@ -17202,6 +17207,13 @@ tmp@^0.0.33: ...@@ -17202,6 +17207,13 @@ tmp@^0.0.33:
dependencies: dependencies:
os-tmpdir "~1.0.2" os-tmpdir "~1.0.2"
tmp@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==
dependencies:
rimraf "^2.6.3"
tmpl@1.0.x: tmpl@1.0.x:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment