1 | 'use strict';
|
2 |
|
3 | var fs = require('fs-extra');
|
4 | var chalk = require('chalk');
|
5 | var diff$1 = require('diff');
|
6 | var path = require('path');
|
7 | var inquirer = require('inquirer');
|
8 | var handlebars = require('handlebars');
|
9 | var recursive = require('recursive-readdir');
|
10 | var index = require('./index-09611511.cjs.js');
|
11 | require('commander');
|
12 | require('semver');
|
13 | require('@backstage/cli-common');
|
14 | require('@backstage/errors');
|
15 |
|
16 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
17 |
|
18 | var fs__default = _interopDefaultLegacy(fs);
|
19 | var chalk__default = _interopDefaultLegacy(chalk);
|
20 | var inquirer__default = _interopDefaultLegacy(inquirer);
|
21 | var handlebars__default = _interopDefaultLegacy(handlebars);
|
22 | var recursive__default = _interopDefaultLegacy(recursive);
|
23 |
|
24 | function sortObjectKeys(obj) {
|
25 | const sortedKeys = Object.keys(obj).sort();
|
26 | for (const key of sortedKeys) {
|
27 | const value = obj[key];
|
28 | delete obj[key];
|
29 | obj[key] = value;
|
30 | }
|
31 | }
|
32 | class PackageJsonHandler {
|
33 | constructor(writeFunc, prompt, pkg, targetPkg, variant) {
|
34 | this.writeFunc = writeFunc;
|
35 | this.prompt = prompt;
|
36 | this.pkg = pkg;
|
37 | this.targetPkg = targetPkg;
|
38 | this.variant = variant;
|
39 | }
|
40 | static async handler({ path, write, missing, targetContents, templateContents }, prompt, variant) {
|
41 | console.log("Checking package.json");
|
42 | if (missing) {
|
43 | throw new Error(`${path} doesn't exist`);
|
44 | }
|
45 | const pkg = JSON.parse(templateContents);
|
46 | const targetPkg = JSON.parse(targetContents);
|
47 | const handler = new PackageJsonHandler(write, prompt, pkg, targetPkg, variant);
|
48 | await handler.handle();
|
49 | }
|
50 | static async appHandler(file, prompt) {
|
51 | return PackageJsonHandler.handler(file, prompt, "app");
|
52 | }
|
53 | async handle() {
|
54 | await this.syncField("main");
|
55 | if (this.variant !== "app") {
|
56 | await this.syncField("main:src");
|
57 | }
|
58 | await this.syncField("types");
|
59 | await this.syncFiles();
|
60 | await this.syncScripts();
|
61 | await this.syncPublishConfig();
|
62 | await this.syncDependencies("dependencies");
|
63 | await this.syncDependencies("peerDependencies", true);
|
64 | await this.syncDependencies("devDependencies");
|
65 | await this.syncReactDeps();
|
66 | }
|
67 | async syncField(fieldName, obj = this.pkg, targetObj = this.targetPkg, prefix, sort, optional) {
|
68 | const fullFieldName = chalk__default["default"].cyan(prefix ? `${prefix}[${fieldName}]` : fieldName);
|
69 | const newValue = obj[fieldName];
|
70 | const coloredNewValue = chalk__default["default"].cyan(JSON.stringify(newValue));
|
71 | if (fieldName in targetObj) {
|
72 | const oldValue = targetObj[fieldName];
|
73 | if (JSON.stringify(oldValue) === JSON.stringify(newValue)) {
|
74 | return;
|
75 | }
|
76 | const coloredOldValue = chalk__default["default"].cyan(JSON.stringify(oldValue));
|
77 | const msg = `package.json has mismatched field, ${fullFieldName}, change from ${coloredOldValue} to ${coloredNewValue}?`;
|
78 | if (await this.prompt(msg)) {
|
79 | targetObj[fieldName] = newValue;
|
80 | if (sort) {
|
81 | sortObjectKeys(targetObj);
|
82 | }
|
83 | await this.write();
|
84 | }
|
85 | } else if (fieldName in obj && optional !== true) {
|
86 | if (await this.prompt(`package.json is missing field ${fullFieldName}, set to ${coloredNewValue}?`)) {
|
87 | targetObj[fieldName] = newValue;
|
88 | if (sort) {
|
89 | sortObjectKeys(targetObj);
|
90 | }
|
91 | await this.write();
|
92 | }
|
93 | }
|
94 | }
|
95 | async syncFiles() {
|
96 | const { configSchema } = this.targetPkg;
|
97 | const hasSchemaFile = typeof configSchema === "string";
|
98 | if (!this.targetPkg.files) {
|
99 | const expected = hasSchemaFile ? ["dist", configSchema] : ["dist"];
|
100 | if (await this.prompt(`package.json is missing field "files", set to ${JSON.stringify(expected)}?`)) {
|
101 | this.targetPkg.files = expected;
|
102 | await this.write();
|
103 | }
|
104 | } else {
|
105 | const missing = [];
|
106 | if (!this.targetPkg.files.includes("dist")) {
|
107 | missing.push("dist");
|
108 | }
|
109 | if (hasSchemaFile && !this.targetPkg.files.includes(configSchema)) {
|
110 | missing.push(configSchema);
|
111 | }
|
112 | if (missing.length) {
|
113 | if (await this.prompt(`package.json is missing ${JSON.stringify(missing)} in the "files" field, add?`)) {
|
114 | this.targetPkg.files.push(...missing);
|
115 | await this.write();
|
116 | }
|
117 | }
|
118 | }
|
119 | }
|
120 | async syncScripts() {
|
121 | const pkgScripts = this.pkg.scripts;
|
122 | const targetScripts = this.targetPkg.scripts = this.targetPkg.scripts || {};
|
123 | if (!pkgScripts) {
|
124 | return;
|
125 | }
|
126 | const hasNewScript = Object.values(targetScripts).some((script) => String(script).includes("backstage-cli package "));
|
127 | if (hasNewScript) {
|
128 | return;
|
129 | }
|
130 | for (const key of Object.keys(pkgScripts)) {
|
131 | await this.syncField(key, pkgScripts, targetScripts, "scripts");
|
132 | }
|
133 | }
|
134 | async syncPublishConfig() {
|
135 | const pkgPublishConf = this.pkg.publishConfig;
|
136 | const targetPublishConf = this.targetPkg.publishConfig;
|
137 | if (!pkgPublishConf) {
|
138 | return;
|
139 | }
|
140 | if (!targetPublishConf) {
|
141 | if (await this.prompt("Missing publishConfig, do you want to add it?")) {
|
142 | this.targetPkg.publishConfig = pkgPublishConf;
|
143 | await this.write();
|
144 | }
|
145 | return;
|
146 | }
|
147 | for (const key of Object.keys(pkgPublishConf)) {
|
148 | if (!["access", "registry"].includes(key)) {
|
149 | await this.syncField(key, pkgPublishConf, targetPublishConf, "publishConfig");
|
150 | }
|
151 | }
|
152 | }
|
153 | async syncDependencies(fieldName, required = false) {
|
154 | const pkgDeps = this.pkg[fieldName];
|
155 | const targetDeps = this.targetPkg[fieldName] = this.targetPkg[fieldName] || {};
|
156 | if (!pkgDeps && !required) {
|
157 | return;
|
158 | }
|
159 | await this.syncField("@backstage/core", {}, targetDeps, fieldName, true);
|
160 | await this.syncField("@backstage/core-api", {}, targetDeps, fieldName, true);
|
161 | for (const key of Object.keys(pkgDeps)) {
|
162 | if (this.variant === "app" && key.startsWith("plugin-")) {
|
163 | continue;
|
164 | }
|
165 | await this.syncField(key, pkgDeps, targetDeps, fieldName, true, !required);
|
166 | }
|
167 | }
|
168 | async syncReactDeps() {
|
169 | const targetDeps = this.targetPkg.dependencies = this.targetPkg.dependencies || {};
|
170 | await this.syncField("react", {}, targetDeps, "dependencies");
|
171 | await this.syncField("react-dom", {}, targetDeps, "dependencies");
|
172 | }
|
173 | async write() {
|
174 | await this.writeFunc(`${JSON.stringify(this.targetPkg, null, 2)}
|
175 | `);
|
176 | }
|
177 | }
|
178 | async function exactMatchHandler({ path, write, missing, targetContents, templateContents }, prompt) {
|
179 | console.log(`Checking ${path}`);
|
180 | const coloredPath = chalk__default["default"].cyan(path);
|
181 | if (missing) {
|
182 | if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) {
|
183 | await write(templateContents);
|
184 | }
|
185 | return;
|
186 | }
|
187 | if (targetContents === templateContents) {
|
188 | return;
|
189 | }
|
190 | const diffs = diff$1.diffLines(targetContents, templateContents);
|
191 | for (const diff of diffs) {
|
192 | if (diff.added) {
|
193 | process.stdout.write(chalk__default["default"].green(`+${diff.value}`));
|
194 | } else if (diff.removed) {
|
195 | process.stdout.write(chalk__default["default"].red(`-${diff.value}`));
|
196 | } else {
|
197 | process.stdout.write(` ${diff.value}`);
|
198 | }
|
199 | }
|
200 | if (await prompt(`Outdated ${coloredPath}, do you want to apply the above patch?`)) {
|
201 | await write(templateContents);
|
202 | }
|
203 | }
|
204 | async function existsHandler({ path, write, missing, templateContents }, prompt) {
|
205 | console.log(`Making sure ${path} exists`);
|
206 | const coloredPath = chalk__default["default"].cyan(path);
|
207 | if (missing) {
|
208 | if (await prompt(`Missing ${coloredPath}, do you want to add it?`)) {
|
209 | await write(templateContents);
|
210 | }
|
211 | return;
|
212 | }
|
213 | }
|
214 | async function skipHandler({ path }) {
|
215 | console.log(`Skipping ${path}`);
|
216 | }
|
217 | const handlers = {
|
218 | skip: skipHandler,
|
219 | exists: existsHandler,
|
220 | exactMatch: exactMatchHandler,
|
221 | packageJson: PackageJsonHandler.handler,
|
222 | appPackageJson: PackageJsonHandler.appHandler
|
223 | };
|
224 | async function handleAllFiles(fileHandlers, files, promptFunc) {
|
225 | for (const file of files) {
|
226 | const path$1 = file.path.split(path.sep).join(path.posix.sep);
|
227 | const fileHandler = fileHandlers.find((handler) => handler.patterns.some((pattern) => typeof pattern === "string" ? pattern === path$1 : pattern.test(path$1)));
|
228 | if (fileHandler) {
|
229 | await fileHandler.handler(file, promptFunc);
|
230 | } else {
|
231 | throw new Error(`No template file handler found for ${path$1}`);
|
232 | }
|
233 | }
|
234 | }
|
235 |
|
236 | const inquirerPromptFunc = async (msg) => {
|
237 | const { result } = await inquirer__default["default"].prompt({
|
238 | type: "confirm",
|
239 | name: "result",
|
240 | message: chalk__default["default"].blue(msg)
|
241 | });
|
242 | return result;
|
243 | };
|
244 | const makeCheckPromptFunc = () => {
|
245 | let failed = false;
|
246 | const promptFunc = async (msg) => {
|
247 | failed = true;
|
248 | console.log(chalk__default["default"].red(`[Check Failed] ${msg}`));
|
249 | return false;
|
250 | };
|
251 | const finalize = () => {
|
252 | if (failed) {
|
253 | throw new Error("Check failed, the plugin is not in sync with the latest template");
|
254 | }
|
255 | };
|
256 | return [promptFunc, finalize];
|
257 | };
|
258 | const yesPromptFunc = async (msg) => {
|
259 | console.log(`Accepting: "${msg}"`);
|
260 | return true;
|
261 | };
|
262 |
|
263 | async function readTemplateFile(templateFile, templateVars) {
|
264 | const contents = await fs__default["default"].readFile(templateFile, "utf8");
|
265 | if (!templateFile.endsWith(".hbs")) {
|
266 | return contents;
|
267 | }
|
268 | const packageVersionProvider = index.createPackageVersionProvider(void 0);
|
269 | return handlebars__default["default"].compile(contents)(templateVars, {
|
270 | helpers: {
|
271 | versionQuery(name, hint) {
|
272 | return packageVersionProvider(name, typeof hint === "string" ? hint : void 0);
|
273 | }
|
274 | }
|
275 | });
|
276 | }
|
277 | async function readTemplate(templateDir, templateVars) {
|
278 | const templateFilePaths = await recursive__default["default"](templateDir).catch((error) => {
|
279 | throw new Error(`Failed to read template directory: ${error.message}`);
|
280 | });
|
281 | const templatedFiles = new Array();
|
282 | for (const templateFile of templateFilePaths) {
|
283 | const path$1 = path.relative(templateDir, templateFile).replace(/\.hbs$/, "");
|
284 | const contents = await readTemplateFile(templateFile, templateVars);
|
285 | templatedFiles.push({ path: path$1, contents });
|
286 | }
|
287 | return templatedFiles;
|
288 | }
|
289 | async function diffTemplatedFiles(targetDir, templatedFiles) {
|
290 | const fileDiffs = new Array();
|
291 | for (const { path: path$1, contents: templateContents } of templatedFiles) {
|
292 | const targetPath = path.resolve(targetDir, path$1);
|
293 | const targetExists = await fs__default["default"].pathExists(targetPath);
|
294 | const write = async (contents) => {
|
295 | await fs__default["default"].ensureDir(path.dirname(targetPath));
|
296 | await fs__default["default"].writeFile(targetPath, contents, "utf8");
|
297 | };
|
298 | if (targetExists) {
|
299 | const targetContents = await fs__default["default"].readFile(targetPath, "utf8");
|
300 | fileDiffs.push({
|
301 | path: path$1,
|
302 | write,
|
303 | missing: false,
|
304 | targetContents,
|
305 | templateContents
|
306 | });
|
307 | } else {
|
308 | fileDiffs.push({
|
309 | path: path$1,
|
310 | write,
|
311 | missing: true,
|
312 | targetContents: "",
|
313 | templateContents
|
314 | });
|
315 | }
|
316 | }
|
317 | return fileDiffs;
|
318 | }
|
319 | async function diffTemplateFiles(template, templateData) {
|
320 | const templateDir = index.paths.resolveOwn("templates", template);
|
321 | const templatedFiles = await readTemplate(templateDir, templateData);
|
322 | const fileDiffs = await diffTemplatedFiles(index.paths.targetDir, templatedFiles);
|
323 | return fileDiffs;
|
324 | }
|
325 |
|
326 | const fileHandlers = [
|
327 | {
|
328 | patterns: ["package.json"],
|
329 | handler: handlers.packageJson
|
330 | },
|
331 | {
|
332 | patterns: [".eslintrc.js"],
|
333 | handler: handlers.exists
|
334 | },
|
335 | {
|
336 | patterns: ["README.md", "tsconfig.json", /^src\
|
337 | handler: handlers.skip
|
338 | }
|
339 | ];
|
340 | var diff = async (opts) => {
|
341 | let promptFunc = inquirerPromptFunc;
|
342 | let finalize = () => {
|
343 | };
|
344 | if (opts.check) {
|
345 | [promptFunc, finalize] = makeCheckPromptFunc();
|
346 | } else if (opts.yes) {
|
347 | promptFunc = yesPromptFunc;
|
348 | }
|
349 | const data = await readPluginData();
|
350 | const templateFiles = await diffTemplateFiles("default-plugin", data);
|
351 | await handleAllFiles(fileHandlers, templateFiles, promptFunc);
|
352 | finalize();
|
353 | };
|
354 | async function readPluginData() {
|
355 | let name;
|
356 | let privatePackage;
|
357 | let pluginVersion;
|
358 | let npmRegistry;
|
359 | try {
|
360 | const pkg = require(index.paths.resolveTarget("package.json"));
|
361 | name = pkg.name;
|
362 | privatePackage = pkg.private;
|
363 | pluginVersion = pkg.version;
|
364 | const scope = name.split("/")[0];
|
365 | if (`${scope}:registry` in pkg.publishConfig) {
|
366 | const registryURL = pkg.publishConfig[`${scope}:registry`];
|
367 | npmRegistry = `"${scope}:registry" : "${registryURL}"`;
|
368 | } else
|
369 | npmRegistry = "";
|
370 | } catch (error) {
|
371 | throw new Error(`Failed to read target package, ${error}`);
|
372 | }
|
373 | const pluginTsContents = await fs__default["default"].readFile(index.paths.resolveTarget("src/plugin.ts"), "utf8");
|
374 | const pluginIdMatch = pluginTsContents.match(/id: ['"`](.+?)['"`]/);
|
375 | if (!pluginIdMatch) {
|
376 | throw new Error(`Failed to parse plugin.ts, no plugin ID found`);
|
377 | }
|
378 | const id = pluginIdMatch[1];
|
379 | return { id, name, privatePackage, pluginVersion, npmRegistry };
|
380 | }
|
381 |
|
382 | exports["default"] = diff;
|
383 |
|