@flarum/cli 3.0.11 → 3.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +33 -1
  2. package/bin/libgit2.1.5.dylib +0 -0
  3. package/bin/splitsh-lite-darwin-arm64 +0 -0
  4. package/boilerplate/skeleton/extension/composer.json +4 -0
  5. package/boilerplate/skeleton/extension/js/package.json +5 -5
  6. package/boilerplate/skeleton/extension/js/tsconfig.json +3 -0
  7. package/boilerplate/skeleton/extension/js/tsconfig.test.json +1 -0
  8. package/boilerplate/skeleton/extension/phpstan.neon +0 -1
  9. package/boilerplate/skeleton/extension/tests/phpunit.integration.xml +1 -0
  10. package/boilerplate/skeleton/extension/tests/phpunit.unit.xml +2 -1
  11. package/boilerplate/stubs/backend/advanced-api-resource.php +3 -3
  12. package/boilerplate/stubs/backend/model-factory.php +17 -0
  13. package/boilerplate/stubs/backend/model.php +3 -0
  14. package/lib/base-command.d.ts +21 -3
  15. package/lib/base-command.js +52 -5
  16. package/lib/commands/make/backend/model.js +10 -0
  17. package/lib/commands/monorepo/split.js +1 -1
  18. package/lib/commands/upgrade/2.0.d.ts +43 -0
  19. package/lib/commands/upgrade/2.0.js +128 -2
  20. package/lib/steps/gen-ext-scaffolder.d.ts +5 -0
  21. package/lib/steps/gen-ext-scaffolder.js +17 -2
  22. package/lib/steps/monorepo/split.d.ts +2 -1
  23. package/lib/steps/monorepo/split.js +20 -5
  24. package/lib/steps/stubs/backend/advanced-api-resource.js +1 -1
  25. package/lib/steps/stubs/backend/model-factory.d.ts +24 -0
  26. package/lib/steps/stubs/backend/model-factory.js +33 -0
  27. package/lib/steps/upgrade/twopointoh/backend/email-views.js +22 -30
  28. package/lib/steps/upgrade/twopointoh/backend/extension-models.d.ts +30 -0
  29. package/lib/steps/upgrade/twopointoh/backend/extension-models.js +34 -0
  30. package/lib/steps/upgrade/twopointoh/backend/json-api.d.ts +6 -0
  31. package/lib/steps/upgrade/twopointoh/backend/json-api.js +14 -0
  32. package/lib/steps/upgrade/twopointoh/backend/misc.d.ts +6 -0
  33. package/lib/steps/upgrade/twopointoh/backend/misc.js +19 -7
  34. package/lib/steps/upgrade/twopointoh/backend/model-factories.d.ts +18 -0
  35. package/lib/steps/upgrade/twopointoh/backend/model-factories.js +73 -0
  36. package/lib/steps/upgrade/twopointoh/backend/phpunit-xml.d.ts +8 -0
  37. package/lib/steps/upgrade/twopointoh/backend/phpunit-xml.js +33 -0
  38. package/lib/steps/upgrade/twopointoh/backend/phpunit.d.ts +2 -0
  39. package/lib/steps/upgrade/twopointoh/backend/phpunit.js +34 -21
  40. package/lib/steps/upgrade/twopointoh/backend/search.d.ts +9 -0
  41. package/lib/steps/upgrade/twopointoh/backend/search.js +28 -10
  42. package/lib/steps/upgrade/twopointoh/base.d.ts +38 -0
  43. package/lib/steps/upgrade/twopointoh/base.js +122 -24
  44. package/lib/steps/upgrade/twopointoh/dependencies.d.ts +58 -1
  45. package/lib/steps/upgrade/twopointoh/dependencies.js +343 -15
  46. package/lib/steps/upgrade/twopointoh/frontend/admin-extender.d.ts +86 -0
  47. package/lib/steps/upgrade/twopointoh/frontend/admin-extender.js +842 -0
  48. package/lib/steps/upgrade/twopointoh/frontend/code-splitting/usage-of-lazy-modules.js +110 -18
  49. package/lib/steps/upgrade/twopointoh/frontend/export-registry/import-ext.js +13 -16
  50. package/lib/steps/upgrade/twopointoh/frontend/misc.d.ts +23 -0
  51. package/lib/steps/upgrade/twopointoh/frontend/misc.js +147 -8
  52. package/lib/steps/upgrade/twopointoh/infra.d.ts +16 -1
  53. package/lib/steps/upgrade/twopointoh/infra.js +87 -5
  54. package/lib/steps/upgrade/twopointoh/step-failures.d.ts +32 -0
  55. package/lib/steps/upgrade/twopointoh/step-failures.js +54 -0
  56. package/lib/steps/upgrade/twopointoh/todos.d.ts +22 -0
  57. package/lib/steps/upgrade/twopointoh/todos.js +57 -0
  58. package/lib/steps/upgrade/twopointoh/typescript-4-9.d.ts +12 -0
  59. package/lib/steps/upgrade/twopointoh/typescript-4-9.js +200 -0
  60. package/lib/utils/ast.js +5 -10
  61. package/lib/utils/babel-ts.d.ts +9 -0
  62. package/lib/utils/babel-ts.js +18 -0
  63. package/lib/utils/composer.d.ts +5 -0
  64. package/lib/utils/composer.js +10 -1
  65. package/lib/utils/terminal.d.ts +13 -0
  66. package/lib/utils/terminal.js +26 -0
  67. package/oclif.manifest.json +1 -1
  68. package/package.json +1 -1
  69. package/php-subsystem/index.php +12 -0
  70. package/php-subsystem/src/NodeUtil.php +19 -9
  71. package/php-subsystem/src/NodeVisitors/AddExtender.php +49 -1
  72. package/php-subsystem/src/Upgrade/TwoPointOh/JsonApi.php +2 -20
  73. package/php-subsystem/src/Upgrade/TwoPointOh/Misc.php +13 -17
  74. package/php-subsystem/src/Upgrade/TwoPointOh/ModelFactories.php +943 -0
  75. package/php-subsystem/src/Upgrade/TwoPointOh/PhpUnit.php +476 -71
  76. package/php-subsystem/src/Upgrade/TwoPointOh/Search.php +97 -13
@@ -12,10 +12,24 @@ const chalk_1 = tslib_1.__importDefault(require("chalk"));
12
12
  const string_1 = tslib_1.__importDefault(require("string"));
13
13
  const simple_git_1 = tslib_1.__importDefault(require("simple-git"));
14
14
  const types_1 = require("@babel/types");
15
+ const step_failures_1 = require("./step-failures");
16
+ const todos_1 = require("./todos");
17
+ const terminal_1 = require("../../../utils/terminal");
18
+ /**
19
+ * The indentation a JSON file's first indented line uses, or the given
20
+ * fallback when it has none (an empty object, say, or everything on one line).
21
+ */
22
+ function jsonIndentation(code, fallback) {
23
+ var _a, _b;
24
+ return (_b = (_a = code.match(/^[\t ]+(?=\S)/m)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : fallback;
25
+ }
15
26
  class BaseUpgradeStep {
16
27
  getExposed(_paths, _paramProvider) {
17
28
  return (0, pick_deep_1.default)(this.params, this.exposes);
18
29
  }
30
+ get number() {
31
+ return this.step;
32
+ }
19
33
  constructor(command, root, step, totalSteps, forceStep) {
20
34
  this.composable = true;
21
35
  this.exposes = [];
@@ -24,7 +38,15 @@ class BaseUpgradeStep {
24
38
  this.totalSteps = 0;
25
39
  this.forceStep = false;
26
40
  this.beforeHook = false;
41
+ /** Whether the step runs for a check, in a worktree that is thrown away. */
42
+ this.checking = false;
43
+ /** Work this step leaves that no @TODO comment in a file shows. */
44
+ this.followUps = [];
27
45
  this.fsEditor = null;
46
+ /**
47
+ * Unset until the step runs.
48
+ */
49
+ this.outcome = null;
28
50
  this.command = command;
29
51
  this.step = step;
30
52
  this.totalSteps = totalSteps;
@@ -34,14 +56,29 @@ class BaseUpgradeStep {
34
56
  before(_file, _code, _advanced) {
35
57
  // ...
36
58
  }
59
+ /**
60
+ * Leave work for the author that the step can't mark with a @TODO comment,
61
+ * such as a file that is missing, or a change it can only point out.
62
+ */
63
+ leaveTodo(file, text) {
64
+ if (!this.followUps.some((todo) => todo.file === file && todo.text === text)) {
65
+ this.followUps.push({ file, line: null, text });
66
+ }
67
+ }
68
+ /** The follow-ups, as lines to print under the step's notes. */
69
+ followUpLines() {
70
+ return this.followUps.map((todo) => `${chalk_1.default.bgYellow.black(' TO DO ')} ${todo.file}: ${todo.text}`);
71
+ }
37
72
  async run(fs, paths, io, providers) {
38
- var _a, _b, _c, _d, _e;
73
+ var _a, _b, _c, _d;
39
74
  if (this.root !== paths.cwd()) {
40
75
  process.chdir(this.root);
41
76
  }
42
77
  const fsEditor = (0, mem_fs_editor_1.create)(fs);
43
78
  this.fsEditor = fsEditor;
44
79
  this.php = providers.php;
80
+ // Until the step gets further: whatever stops it from here on is a failure.
81
+ this.outcome = { status: 'failed', failures: [] };
45
82
  // Stop if there are uncommited changes
46
83
  const status = await (0, simple_git_1.default)((_a = paths.requestedDir()) !== null && _a !== void 0 ? _a : paths.cwd()).status();
47
84
  if (status === null || status === void 0 ? void 0 : status.files.length) {
@@ -56,20 +93,24 @@ class BaseUpgradeStep {
56
93
  if (!this.forceStep && (await this.alreadyCommited((_b = paths.requestedDir()) !== null && _b !== void 0 ? _b : paths.cwd(), this.gitCommit().message))) {
57
94
  this.command.log(' => ' + chalk_1.default.bgGreen.bold(' SKIP '));
58
95
  this.command.log('');
96
+ this.outcome = { status: 'skipped' };
59
97
  return fs;
60
98
  }
61
99
  // Show loading indicator
62
100
  let dots = 1;
63
101
  let nl = false;
64
102
  const progress = () => {
103
+ if (!(0, terminal_1.isTerminal)())
104
+ return;
65
105
  if (!nl) {
66
106
  this.command.log('');
67
107
  nl = true;
68
108
  }
69
109
  const filledDots = '.'.repeat(dots) + ' '.repeat(3 - dots);
70
- this.command.log('\u001B[A => ' + chalk_1.default.bold(' WORKING' + filledDots));
110
+ this.command.log((0, terminal_1.overwrite)() + ' => ' + chalk_1.default.bold(' WORKING' + filledDots));
71
111
  dots = (dots + 1) % 4;
72
112
  };
113
+ const failures = new step_failures_1.StepFailures();
73
114
  const targets = this.targets();
74
115
  for (const target of targets) {
75
116
  progress();
@@ -84,8 +125,15 @@ class BaseUpgradeStep {
84
125
  continue;
85
126
  }
86
127
  const code = fsEditor.read(file);
87
- const advanced = this.advancedContent(file, code);
88
- this.before(file, code, advanced);
128
+ // Same as the transform pass below: a file this step can't read
129
+ // shouldn't stop it from reporting on the others.
130
+ try {
131
+ const advanced = this.advancedContent(file, code);
132
+ this.before(file, code, advanced);
133
+ }
134
+ catch (error) {
135
+ failures.record((0, string_1.default)(file.replace(paths.package(), '')).stripLeft('/').stripLeft('\\').toString(), error);
136
+ }
89
137
  progress();
90
138
  }
91
139
  }
@@ -97,10 +145,20 @@ class BaseUpgradeStep {
97
145
  continue;
98
146
  }
99
147
  const code = fsEditor.read(file);
100
- const advanced = this.advancedContent(file, code);
101
148
  const relativeTarget = (0, string_1.default)(file.replace(paths.package(), '')).stripLeft('/').stripLeft('\\').toString();
102
- // eslint-disable-next-line no-await-in-loop
103
- const result = await this.applyReplacements(relativeTarget, code, advanced);
149
+ let result;
150
+ // One file the step can't handle shouldn't decide the fate of the
151
+ // rest: note it and carry on, so the author is told about every
152
+ // problem this step found rather than the first one.
153
+ try {
154
+ const advanced = this.advancedContent(file, code);
155
+ // eslint-disable-next-line no-await-in-loop
156
+ result = await this.applyReplacements(relativeTarget, code, advanced);
157
+ }
158
+ catch (error) {
159
+ failures.record(relativeTarget, error);
160
+ continue;
161
+ }
104
162
  if (result.newPath && result.newPath !== file) {
105
163
  fsEditor.move(file, result.newPath);
106
164
  newFiles.push(result.newPath);
@@ -137,20 +195,49 @@ class BaseUpgradeStep {
137
195
  // await applyOn(newFiles);
138
196
  // }
139
197
  }
140
- const changesMade = await (0, simple_git_1.default)((_c = paths.requestedDir()) !== null && _c !== void 0 ? _c : paths.cwd())
198
+ const dir = (_c = paths.requestedDir()) !== null && _c !== void 0 ? _c : paths.cwd();
199
+ if (failures.any()) {
200
+ // Leave the step all-or-nothing. Its partial writes are already on disk,
201
+ // and the run refuses to start with a dirty tree, so keeping them would
202
+ // block the re-run this step is asking the author to do.
203
+ await (0, simple_git_1.default)(dir).checkout(['--', '.']);
204
+ this.outcome = { status: 'failed', failures: failures.list() };
205
+ this.command.log((0, terminal_1.overwrite)() + ' => ' + chalk_1.default.bgRed.bold(' FAILED '));
206
+ this.command.log('');
207
+ this.command.log(failures.report());
208
+ this.command.log('');
209
+ const files = failures.count() === 1 ? 'file' : 'files';
210
+ this.command.error(`${failures.count()} ${files} could not be upgraded by this step, so it made no changes.\n` +
211
+ ' Fix the problems above and run the command again — steps that already\n' +
212
+ ' completed are skipped, so it will resume from here.', { code: 'FL_ERR' });
213
+ }
214
+ const changesMade = await (0, simple_git_1.default)(dir)
141
215
  .diffSummary()
142
216
  .then((summary) => summary.files.length > 0);
143
217
  if (changesMade) {
144
218
  await (0, simple_git_1.default)((_d = paths.requestedDir()) !== null && _d !== void 0 ? _d : paths.cwd()).add('.');
145
- await this.pauseAndConfirm(this.pauseMessage(), io);
219
+ const notes = this.pauseMessage();
220
+ await this.pauseAndConfirm([notes, ...this.followUpLines()].join('\n '), io);
146
221
  const commit = this.gitCommit();
147
222
  const message = commit.message + '\n\n' + commit.description;
148
- await (0, create_1.commitAll)((_e = paths.requestedDir()) !== null && _e !== void 0 ? _e : paths.cwd(), message);
149
- this.command.log('\u001B[A => ' + chalk_1.default.bgCyan.bold(' COMMIT ') + ' ' + chalk_1.default.dim(commit.message));
223
+ await (0, create_1.commitAll)(dir, message);
224
+ const sha = (await (0, simple_git_1.default)(dir).revparse(['HEAD'])).trim();
225
+ this.outcome = {
226
+ status: 'committed',
227
+ commit: { sha, message: commit.message },
228
+ notes: (0, terminal_1.stripAnsi)(notes).trim(),
229
+ todos: [...(await (0, todos_1.todosAddedBy)(dir, sha)), ...this.followUps],
230
+ };
231
+ const label = this.checking ? chalk_1.default.bgYellow.black.bold(' WOULD CHANGE ') : chalk_1.default.bgCyan.bold(' COMMIT ');
232
+ this.command.log((0, terminal_1.overwrite)() + ' => ' + label + ' ' + chalk_1.default.dim(commit.message));
150
233
  this.command.log('');
151
234
  }
152
235
  else {
153
- this.command.log('\u001B[A => ' + chalk_1.default.bgGreen.bold(' NO CHANGES '));
236
+ this.outcome = this.followUps.length > 0 ? { status: 'no-changes', todos: this.followUps } : { status: 'no-changes' };
237
+ this.command.log((0, terminal_1.overwrite)() + ' => ' + chalk_1.default.bgGreen.bold(' NO CHANGES '));
238
+ for (const line of this.followUpLines()) {
239
+ this.command.log(' ' + line);
240
+ }
154
241
  this.command.log('');
155
242
  }
156
243
  return fs;
@@ -176,13 +263,17 @@ class BaseUpgradeStep {
176
263
  continue;
177
264
  // eslint-disable-next-line no-await-in-loop
178
265
  code = await this.updateCode(file, code, result.updated);
266
+ // Later replacements read the parsed code: parse it again after a text
267
+ // change, or one that returns a syntax tree would undo this change.
268
+ if (typeof result.updated === 'string') {
269
+ advanced = this.advancedContent(file, code);
270
+ }
179
271
  if (((_a = result.imports) === null || _a === void 0 ? void 0 : _a.length) || (result.updated && typeof result.updated !== 'string' && 'program' in result.updated)) {
180
272
  try {
181
273
  advanced = (0, ast_1.parseCode)(code);
182
274
  }
183
275
  catch (error) {
184
- this.command.warn(`Failed to parse code for ${file}, code: \n${code}`);
185
- throw error;
276
+ throw new Error(`The transformed code could not be parsed back: ${error.message}`);
186
277
  }
187
278
  (_b = result.imports) === null || _b === void 0 ? void 0 : _b.forEach((imp) => {
188
279
  this.ensureImport(file, advanced, imp);
@@ -230,17 +321,22 @@ class BaseUpgradeStep {
230
321
  if (lang === 'json') {
231
322
  // remove comments
232
323
  code = code.replace(/^\s*\/\/.*$/gm, '');
233
- // remove loose ,
234
- code = code.replace(/,\s*}/g, '}');
324
+ // remove loose , before the end of an object or an array, as tsconfig.json allows
325
+ code = code.replace(/,(\s*[\]}])/g, '$1');
235
326
  try {
236
327
  return JSON.parse(code);
237
328
  }
238
- catch (_a) {
239
- this.command.error(`Failed to parse JSON file ${file}`);
329
+ catch (error) {
330
+ throw new Error(`Not valid JSON: ${error.message}`);
240
331
  }
241
332
  }
242
333
  if (['js', 'ts', 'jsx', 'tsx'].includes(lang || '')) {
243
- return (0, ast_1.parseCode)(code);
334
+ try {
335
+ return (0, ast_1.parseCode)(code);
336
+ }
337
+ catch (error) {
338
+ throw new Error(`Could not parse: ${error.message}`);
339
+ }
244
340
  }
245
341
  return null;
246
342
  }
@@ -253,23 +349,25 @@ class BaseUpgradeStep {
253
349
  return updated;
254
350
  }
255
351
  if (lang === 'json') {
256
- const space = file === 'composer.json' ? 4 : 2;
257
- return JSON.stringify(updated, null, space);
352
+ // Write the file back the way it was laid out, so the diff only shows
353
+ // what the step changed.
354
+ const space = jsonIndentation(code, file === 'composer.json' ? 4 : 2);
355
+ const json = JSON.stringify(updated, null, space);
356
+ return code.includes('\r\n') ? json.replace(/\n/g, '\r\n') + (code.endsWith('\r\n') ? '\r\n' : '') : json + (code.endsWith('\n') ? '\n' : '');
258
357
  }
259
358
  if (updated && 'program' in updated) {
260
359
  try {
261
360
  return (0, ast_1.generateCode)(updated, false);
262
361
  }
263
362
  catch (error) {
264
- this.command.warn(`Failed to generate code for ${file}, code: \n${code}`);
265
- throw error;
363
+ throw new Error(`Could not generate code from the transformed syntax tree: ${error.message}`);
266
364
  }
267
365
  }
268
366
  return code;
269
367
  }
270
368
  async pauseAndConfirm(message, io) {
271
369
  const stepMarker = chalk_1.default.bgWhite.black.bold(' DONE ');
272
- this.command.log('\u001B[A => ' + stepMarker + ' ' + message);
370
+ this.command.log((0, terminal_1.overwrite)() + ' => ' + stepMarker + ' ' + message);
273
371
  this.command.log('');
274
372
  await this.command.continueWhenReady(io);
275
373
  }
@@ -1,10 +1,67 @@
1
- import { BaseUpgradeStep, GitCommit, Replacement } from './base';
1
+ import { AdvancedContent, BaseUpgradeStep, GitCommit, Replacement } from './base';
2
2
  export default class Dependencies extends BaseUpgradeStep {
3
3
  type: string;
4
+ /** The TypeScript the extension asked for, when it was not a 4.x. */
5
+ protected typeScriptWas: string | null;
6
+ /** What was removed from tsconfig.json, as TypeScript 4.9 doesn't have it. */
7
+ protected removedCompilerOptions: string[];
8
+ protected newerSyntax: boolean;
9
+ /** Constraints left as they were, as they already ask for newer versions. */
10
+ protected keptNewer: string[];
11
+ /** Tooling moved from dependencies to devDependencies. */
12
+ protected movedToDev: string[];
13
+ /** The typings scripts, and the tooling they run, added to package.json. */
14
+ protected addedTypings: string[];
4
15
  replacements(file: string): Replacement[];
5
16
  targets(): string[];
17
+ advancedContent(file: string, code: string): AdvancedContent;
18
+ updateCode(file: string, code: string, updated: string | AdvancedContent): Promise<string>;
6
19
  gitCommit(): GitCommit;
7
20
  pauseMessage(): string;
21
+ /**
22
+ * Require at least the given version, leaving a constraint that already
23
+ * asks for a newer one as it is.
24
+ */
25
+ protected raise(dependencies: Record<string, string>, name: string, version: string): void;
8
26
  updateComposerJson(): Replacement;
9
27
  updatePackageJson(): Replacement;
28
+ /**
29
+ * Add the scripts that build and check typings, which a new extension is
30
+ * scaffolded with and the reusable frontend workflow runs, along with the
31
+ * tooling they need. Scripts the extension already has are left alone.
32
+ */
33
+ protected addTypingsScripts(pkg: Record<string, any>): void;
34
+ protected hasTsConfig(): boolean;
35
+ /** The package manager the extension's frontend uses, by its lockfile. */
36
+ protected packageManager(): 'yarn' | 'npm';
37
+ /**
38
+ * Flarum 2.0 extensions only build with their tooling: none of it ends up
39
+ * in the bundle, so it belongs in devDependencies. Where the extension lists
40
+ * a tool under both keys, the newer constraint is kept. Other dependencies
41
+ * may be libraries the bundle includes, so they stay where they are.
42
+ */
43
+ protected moveToolingToDevDependencies(pkg: Record<string, any>): void;
44
+ /**
45
+ * Drop the compiler options, and option values, TypeScript 4.9 doesn't have:
46
+ * it refuses a tsconfig.json with any. A tsconfig.json it can read is left
47
+ * exactly as it was.
48
+ */
49
+ updateTsConfig(): Replacement;
50
+ /**
51
+ * TypeScript loads every type package in node_modules unless told which,
52
+ * and 4.9 can't read the @types/node that webpack's minimizer brings along.
53
+ * Load only the ones the code imports. The file is edited as text so its
54
+ * comments stay.
55
+ */
56
+ limitTypes(): Replacement;
57
+ /**
58
+ * A test config extending tsconfig.json would no longer load Jest's globals.
59
+ */
60
+ testTypes(): Replacement;
61
+ /** The extension's js/package.json, or nothing when it has none. */
62
+ protected packageJson(): Record<string, any>;
63
+ /**
64
+ * Put a @TODO above each line using syntax TypeScript 4.9 can't compile.
65
+ */
66
+ flagNewerSyntax(): Replacement;
10
67
  }