@flarum/cli 3.0.12 → 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 (66) hide show
  1. package/README.md +33 -1
  2. package/boilerplate/skeleton/extension/composer.json +4 -0
  3. package/boilerplate/skeleton/extension/js/package.json +4 -4
  4. package/boilerplate/skeleton/extension/js/tsconfig.json +3 -0
  5. package/boilerplate/skeleton/extension/js/tsconfig.test.json +1 -0
  6. package/boilerplate/stubs/backend/advanced-api-resource.php +3 -3
  7. package/boilerplate/stubs/backend/model-factory.php +17 -0
  8. package/boilerplate/stubs/backend/model.php +3 -0
  9. package/lib/base-command.d.ts +21 -3
  10. package/lib/base-command.js +52 -5
  11. package/lib/commands/make/backend/model.js +10 -0
  12. package/lib/commands/upgrade/2.0.d.ts +43 -0
  13. package/lib/commands/upgrade/2.0.js +128 -2
  14. package/lib/steps/gen-ext-scaffolder.d.ts +5 -0
  15. package/lib/steps/gen-ext-scaffolder.js +17 -2
  16. package/lib/steps/stubs/backend/advanced-api-resource.js +1 -1
  17. package/lib/steps/stubs/backend/model-factory.d.ts +24 -0
  18. package/lib/steps/stubs/backend/model-factory.js +33 -0
  19. package/lib/steps/upgrade/twopointoh/backend/email-views.js +22 -30
  20. package/lib/steps/upgrade/twopointoh/backend/extension-models.d.ts +30 -0
  21. package/lib/steps/upgrade/twopointoh/backend/extension-models.js +34 -0
  22. package/lib/steps/upgrade/twopointoh/backend/json-api.d.ts +6 -0
  23. package/lib/steps/upgrade/twopointoh/backend/json-api.js +14 -0
  24. package/lib/steps/upgrade/twopointoh/backend/misc.d.ts +6 -0
  25. package/lib/steps/upgrade/twopointoh/backend/misc.js +19 -7
  26. package/lib/steps/upgrade/twopointoh/backend/model-factories.d.ts +18 -0
  27. package/lib/steps/upgrade/twopointoh/backend/model-factories.js +73 -0
  28. package/lib/steps/upgrade/twopointoh/backend/phpunit.d.ts +2 -0
  29. package/lib/steps/upgrade/twopointoh/backend/phpunit.js +16 -8
  30. package/lib/steps/upgrade/twopointoh/backend/search.d.ts +9 -0
  31. package/lib/steps/upgrade/twopointoh/backend/search.js +28 -10
  32. package/lib/steps/upgrade/twopointoh/base.d.ts +38 -0
  33. package/lib/steps/upgrade/twopointoh/base.js +73 -12
  34. package/lib/steps/upgrade/twopointoh/dependencies.d.ts +58 -1
  35. package/lib/steps/upgrade/twopointoh/dependencies.js +342 -19
  36. package/lib/steps/upgrade/twopointoh/frontend/admin-extender.d.ts +86 -0
  37. package/lib/steps/upgrade/twopointoh/frontend/admin-extender.js +842 -0
  38. package/lib/steps/upgrade/twopointoh/frontend/code-splitting/usage-of-lazy-modules.js +110 -18
  39. package/lib/steps/upgrade/twopointoh/frontend/export-registry/import-ext.js +13 -16
  40. package/lib/steps/upgrade/twopointoh/frontend/misc.d.ts +23 -0
  41. package/lib/steps/upgrade/twopointoh/frontend/misc.js +147 -8
  42. package/lib/steps/upgrade/twopointoh/infra.d.ts +16 -1
  43. package/lib/steps/upgrade/twopointoh/infra.js +87 -5
  44. package/lib/steps/upgrade/twopointoh/step-failures.d.ts +8 -0
  45. package/lib/steps/upgrade/twopointoh/step-failures.js +6 -0
  46. package/lib/steps/upgrade/twopointoh/todos.d.ts +22 -0
  47. package/lib/steps/upgrade/twopointoh/todos.js +57 -0
  48. package/lib/steps/upgrade/twopointoh/typescript-4-9.d.ts +12 -0
  49. package/lib/steps/upgrade/twopointoh/typescript-4-9.js +200 -0
  50. package/lib/utils/ast.js +3 -2
  51. package/lib/utils/babel-ts.d.ts +9 -0
  52. package/lib/utils/babel-ts.js +18 -0
  53. package/lib/utils/composer.d.ts +5 -0
  54. package/lib/utils/composer.js +10 -1
  55. package/lib/utils/terminal.d.ts +13 -0
  56. package/lib/utils/terminal.js +26 -0
  57. package/oclif.manifest.json +1 -1
  58. package/package.json +1 -1
  59. package/php-subsystem/index.php +12 -0
  60. package/php-subsystem/src/NodeUtil.php +19 -9
  61. package/php-subsystem/src/NodeVisitors/AddExtender.php +49 -1
  62. package/php-subsystem/src/Upgrade/TwoPointOh/JsonApi.php +2 -20
  63. package/php-subsystem/src/Upgrade/TwoPointOh/Misc.php +13 -17
  64. package/php-subsystem/src/Upgrade/TwoPointOh/ModelFactories.php +943 -0
  65. package/php-subsystem/src/Upgrade/TwoPointOh/PhpUnit.php +476 -71
  66. package/php-subsystem/src/Upgrade/TwoPointOh/Search.php +97 -13
@@ -8,6 +8,8 @@ import { ModuleImport } from '../../../utils/ast';
8
8
  import { Editor } from 'mem-fs-editor';
9
9
  import BaseCommand from '../../../base-command';
10
10
  import { PhpProvider } from '../../../providers/php-provider';
11
+ import { Failure } from './step-failures';
12
+ import { Todo } from './todos';
11
13
  export type ReplacementResult = {
12
14
  imports?: ImportChange[];
13
15
  newPath?: string | null;
@@ -28,6 +30,26 @@ export type GitCommit = {
28
30
  message: string;
29
31
  description: string;
30
32
  };
33
+ /**
34
+ * What a step did, for a program driving the upgrade.
35
+ */
36
+ export type StepOutcome = {
37
+ status: 'skipped';
38
+ } | {
39
+ status: 'no-changes';
40
+ todos?: Todo[];
41
+ } | {
42
+ status: 'committed';
43
+ commit: {
44
+ sha: string;
45
+ message: string;
46
+ };
47
+ notes: string;
48
+ todos: Todo[];
49
+ } | {
50
+ status: 'failed';
51
+ failures: Failure[];
52
+ };
31
53
  export declare abstract class BaseUpgradeStep implements Step<FlarumProviders> {
32
54
  abstract type: string;
33
55
  composable: boolean;
@@ -37,6 +59,7 @@ export declare abstract class BaseUpgradeStep implements Step<FlarumProviders> {
37
59
  protected command: BaseCommand;
38
60
  protected php: null | PhpProvider;
39
61
  protected step: number;
62
+ get number(): number;
40
63
  protected totalSteps: number;
41
64
  protected forceStep: boolean;
42
65
  protected root: string;
@@ -47,7 +70,22 @@ export declare abstract class BaseUpgradeStep implements Step<FlarumProviders> {
47
70
  abstract replacements(file: string, code: string): Replacement[];
48
71
  abstract gitCommit(): GitCommit;
49
72
  abstract pauseMessage(): string;
73
+ /** Whether the step runs for a check, in a worktree that is thrown away. */
74
+ checking: boolean;
75
+ /** Work this step leaves that no @TODO comment in a file shows. */
76
+ protected followUps: Todo[];
77
+ /**
78
+ * Leave work for the author that the step can't mark with a @TODO comment,
79
+ * such as a file that is missing, or a change it can only point out.
80
+ */
81
+ protected leaveTodo(file: string, text: string): void;
82
+ /** The follow-ups, as lines to print under the step's notes. */
83
+ protected followUpLines(): string[];
50
84
  protected fsEditor: null | Editor;
85
+ /**
86
+ * Unset until the step runs.
87
+ */
88
+ outcome: StepOutcome | null;
51
89
  run(fs: Store, paths: Paths, io: IO, providers: FlarumProviders): Promise<Store>;
52
90
  applyReplacements(file: string, code: string, advanced: AdvancedContent): Promise<ReplacementResult>;
53
91
  ensureImport(file: string, advanced: AdvancedContent, imp: ImportChange): void;
@@ -13,10 +13,23 @@ 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
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
+ }
16
26
  class BaseUpgradeStep {
17
27
  getExposed(_paths, _paramProvider) {
18
28
  return (0, pick_deep_1.default)(this.params, this.exposes);
19
29
  }
30
+ get number() {
31
+ return this.step;
32
+ }
20
33
  constructor(command, root, step, totalSteps, forceStep) {
21
34
  this.composable = true;
22
35
  this.exposes = [];
@@ -25,7 +38,15 @@ class BaseUpgradeStep {
25
38
  this.totalSteps = 0;
26
39
  this.forceStep = false;
27
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 = [];
28
45
  this.fsEditor = null;
46
+ /**
47
+ * Unset until the step runs.
48
+ */
49
+ this.outcome = null;
29
50
  this.command = command;
30
51
  this.step = step;
31
52
  this.totalSteps = totalSteps;
@@ -35,14 +56,29 @@ class BaseUpgradeStep {
35
56
  before(_file, _code, _advanced) {
36
57
  // ...
37
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
+ }
38
72
  async run(fs, paths, io, providers) {
39
- var _a, _b, _c, _d, _e;
73
+ var _a, _b, _c, _d;
40
74
  if (this.root !== paths.cwd()) {
41
75
  process.chdir(this.root);
42
76
  }
43
77
  const fsEditor = (0, mem_fs_editor_1.create)(fs);
44
78
  this.fsEditor = fsEditor;
45
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: [] };
46
82
  // Stop if there are uncommited changes
47
83
  const status = await (0, simple_git_1.default)((_a = paths.requestedDir()) !== null && _a !== void 0 ? _a : paths.cwd()).status();
48
84
  if (status === null || status === void 0 ? void 0 : status.files.length) {
@@ -57,18 +93,21 @@ class BaseUpgradeStep {
57
93
  if (!this.forceStep && (await this.alreadyCommited((_b = paths.requestedDir()) !== null && _b !== void 0 ? _b : paths.cwd(), this.gitCommit().message))) {
58
94
  this.command.log(' => ' + chalk_1.default.bgGreen.bold(' SKIP '));
59
95
  this.command.log('');
96
+ this.outcome = { status: 'skipped' };
60
97
  return fs;
61
98
  }
62
99
  // Show loading indicator
63
100
  let dots = 1;
64
101
  let nl = false;
65
102
  const progress = () => {
103
+ if (!(0, terminal_1.isTerminal)())
104
+ return;
66
105
  if (!nl) {
67
106
  this.command.log('');
68
107
  nl = true;
69
108
  }
70
109
  const filledDots = '.'.repeat(dots) + ' '.repeat(3 - dots);
71
- 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));
72
111
  dots = (dots + 1) % 4;
73
112
  };
74
113
  const failures = new step_failures_1.StepFailures();
@@ -162,7 +201,8 @@ class BaseUpgradeStep {
162
201
  // and the run refuses to start with a dirty tree, so keeping them would
163
202
  // block the re-run this step is asking the author to do.
164
203
  await (0, simple_git_1.default)(dir).checkout(['--', '.']);
165
- this.command.log('\u001B[A => ' + chalk_1.default.bgRed.bold(' FAILED '));
204
+ this.outcome = { status: 'failed', failures: failures.list() };
205
+ this.command.log((0, terminal_1.overwrite)() + ' => ' + chalk_1.default.bgRed.bold(' FAILED '));
166
206
  this.command.log('');
167
207
  this.command.log(failures.report());
168
208
  this.command.log('');
@@ -176,15 +216,28 @@ class BaseUpgradeStep {
176
216
  .then((summary) => summary.files.length > 0);
177
217
  if (changesMade) {
178
218
  await (0, simple_git_1.default)((_d = paths.requestedDir()) !== null && _d !== void 0 ? _d : paths.cwd()).add('.');
179
- await this.pauseAndConfirm(this.pauseMessage(), io);
219
+ const notes = this.pauseMessage();
220
+ await this.pauseAndConfirm([notes, ...this.followUpLines()].join('\n '), io);
180
221
  const commit = this.gitCommit();
181
222
  const message = commit.message + '\n\n' + commit.description;
182
- await (0, create_1.commitAll)((_e = paths.requestedDir()) !== null && _e !== void 0 ? _e : paths.cwd(), message);
183
- 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));
184
233
  this.command.log('');
185
234
  }
186
235
  else {
187
- 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
+ }
188
241
  this.command.log('');
189
242
  }
190
243
  return fs;
@@ -210,6 +263,11 @@ class BaseUpgradeStep {
210
263
  continue;
211
264
  // eslint-disable-next-line no-await-in-loop
212
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
+ }
213
271
  if (((_a = result.imports) === null || _a === void 0 ? void 0 : _a.length) || (result.updated && typeof result.updated !== 'string' && 'program' in result.updated)) {
214
272
  try {
215
273
  advanced = (0, ast_1.parseCode)(code);
@@ -263,8 +321,8 @@ class BaseUpgradeStep {
263
321
  if (lang === 'json') {
264
322
  // remove comments
265
323
  code = code.replace(/^\s*\/\/.*$/gm, '');
266
- // remove loose ,
267
- 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');
268
326
  try {
269
327
  return JSON.parse(code);
270
328
  }
@@ -291,8 +349,11 @@ class BaseUpgradeStep {
291
349
  return updated;
292
350
  }
293
351
  if (lang === 'json') {
294
- const space = file === 'composer.json' ? 4 : 2;
295
- 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' : '');
296
357
  }
297
358
  if (updated && 'program' in updated) {
298
359
  try {
@@ -306,7 +367,7 @@ class BaseUpgradeStep {
306
367
  }
307
368
  async pauseAndConfirm(message, io) {
308
369
  const stepMarker = chalk_1.default.bgWhite.black.bold(' DONE ');
309
- this.command.log('\u001B[A => ' + stepMarker + ' ' + message);
370
+ this.command.log((0, terminal_1.overwrite)() + ' => ' + stepMarker + ' ' + message);
310
371
  this.command.log('');
311
372
  await this.command.continueWhenReady(io);
312
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
  }