@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
package/README.md CHANGED
@@ -49,7 +49,7 @@ $ npm install -g @flarum/cli
49
49
  $ flarum-cli COMMAND
50
50
  running command...
51
51
  $ flarum-cli (-v|--version|version)
52
- @flarum/cli/3.0.12 darwin-arm64 node-v26.6.0
52
+ @flarum/cli/3.0.13 darwin-arm64 node-v26.11.1
53
53
  $ flarum-cli --help [COMMAND]
54
54
  USAGE
55
55
  $ flarum-cli COMMAND
@@ -132,6 +132,38 @@ The CLI has different types of commands for different tasks:
132
132
  **Code Updates**: These commands help update extensions for newer versions of Flarum.
133
133
 
134
134
  - `flarum-cli update js-imports [PATH]`: Adds admin/forum/common namespaces to all JS imports from flarum core.
135
+ - `flarum-cli upgrade 2.0 [PATH] [-n|--no-interaction] [--json] [--check] [--step <number>] [--force-steps]`: Upgrades a 1.x extension to Flarum 2.0, one step at a time, committing each step. Steps already committed are skipped, so running it again resumes where it stopped.
136
+
137
+ To drive the upgrade from another program, such as an AI agent, pass `--json`. It asks nothing, and prints a single JSON object when it finishes. It exits non-zero when the upgrade didn't complete:
138
+
139
+ ```json
140
+ {
141
+ "status": "failed",
142
+ "error": "1 file could not be upgraded by this step, so it made no changes. …",
143
+ "messages": [],
144
+ "steps": [
145
+ {
146
+ "step": 13,
147
+ "title": "Prepare for JSON:API changes.",
148
+ "status": "committed",
149
+ "commit": { "sha": "c63dd501…", "message": "chore(2.0): JSON:API changes" },
150
+ "notes": "Flarum 2.0 completely refactors the JSON:API implementation. …",
151
+ "todos": [{ "file": "extend.php", "line": 33, "text": "// @TODO: Replace with the new implementation …" }]
152
+ },
153
+ {
154
+ "step": 14,
155
+ "title": "Intervention Image Library v3",
156
+ "status": "failed",
157
+ "failures": [{ "file": "src/Thumbnailer.php", "reason": "Syntax error, unexpected '{' on line 15" }]
158
+ },
159
+ { "step": 15, "title": "Search & Filter API changes", "status": "not-run" }
160
+ ]
161
+ }
162
+ ```
163
+
164
+ Each step is `committed`, `no-changes`, `skipped` (committed by an earlier run), `failed` or `not-run`. A step that fails on some files lists them and makes no changes, so fix those files, commit, and run the command again. The `todos` are what the upgrade could not change itself: the `@TODO` comments the step's commit added, and work it can only point out, such as a file to add, which has a `line` of `null`. A `no-changes` step lists its `todos` too, when it has any.
165
+
166
+ To see what the upgrade would change without changing anything, pass `--check`, such as for an extension already on Flarum 2.0. The steps run on the extension's last commit, in a temporary git worktree that is removed afterwards, and none is skipped. With `--json`, a step that would change something is `would-change`, with the `files` and `diff` of the change, and the report's `check` names the commit checked and whether changes not yet committed were left out.
135
167
 
136
168
  _And of course, you can always use the help command to see a list of all available commands with their descriptions:_
137
169
 
@@ -49,7 +49,9 @@
49
49
  "@test:integration"
50
50
  ],
51
51
  "test:unit": "phpunit -c tests/phpunit.unit.xml",
52
+ "test:unit:filter": "phpunit -c tests/phpunit.unit.xml --filter",
52
53
  "test:integration": "phpunit -c tests/phpunit.integration.xml",
54
+ "test:integration:filter": "phpunit -c tests/phpunit.integration.xml --filter",
53
55
  "test:setup": "@php tests/integration/setup.php",
54
56
  "analyse:phpstan": "phpstan analyse",
55
57
  "clear-cache:phpstan": "phpstan clear-result-cache"
@@ -57,7 +59,9 @@
57
59
  "scripts-descriptions": {
58
60
  "test": "Runs all tests.",
59
61
  "test:unit": "Runs all unit tests.",
62
+ "test:unit:filter": "Runs unit tests whose name matches: composer test:unit:filter -- ClassName",
60
63
  "test:integration": "Runs all integration tests.",
64
+ "test:integration:filter": "Runs integration tests whose name matches: composer test:integration:filter -- ClassName",
61
65
  "test:setup": "Sets up a database for use with integration tests. Execute this only once.",
62
66
  "analyse:phpstan": "Run static analysis"
63
67
  },
@@ -10,11 +10,11 @@
10
10
  "@flarum/jest-config": "^2.0.0",
11
11
  "@flarum/prettier-config": "^1.0.0",
12
12
  "flarum-tsconfig": "^2.0.0",
13
- "flarum-webpack-config": "^3.0.0",
13
+ "flarum-webpack-config": "^3.0.7",
14
14
  "prettier": "^2.5.1",
15
- "typescript": "^4.5.4",
15
+ "typescript": "^4.9.5",
16
16
  "typescript-coverage-report": "^0.6.1",
17
- "webpack": "^5.65.0",
17
+ "webpack": "^5.104.1",
18
18
  "webpack-cli": "^4.9.1"
19
19
  },
20
20
  "scripts": {
@@ -25,7 +25,7 @@
25
25
  "format-check": "prettier --check src",
26
26
  "clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
27
27
  "build-typings": "<%= params.jsPackageManager %> run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && <%= params.jsPackageManager %> run post-build-typings",
28
- "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'",
28
+ "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i.bak 's,../src/@types,@types,g' && find dist-typings -type f -name '*.d.ts.bak' -delete",
29
29
  "check-typings": "tsc --noEmit --emitDeclarationOnly false",
30
30
  "check-typings-coverage": "typescript-coverage-report",
31
31
  "test": "yarn node --experimental-vm-modules $(yarn bin jest)"
@@ -12,6 +12,9 @@
12
12
  "@types/**/*"
13
13
  ],
14
14
  "compilerOptions": {
15
+ // Only load the type packages the code imports. Others, such as the
16
+ // @types/node webpack brings along, use syntax TypeScript 4.9 can't read.
17
+ "types": [],
15
18
  // This will output typings to `dist-typings`
16
19
  "declarationDir": "./dist-typings",
17
20
  "baseUrl": ".",
@@ -3,6 +3,7 @@
3
3
  "include": ["tests/**/*"],
4
4
  "files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"],
5
5
  "compilerOptions": {
6
+ "types": ["jest"],
6
7
  "strict": false,
7
8
  "noImplicitReturns": false,
8
9
  "noImplicitAny": false
@@ -14,7 +14,7 @@ use Tobyz\JsonApiServer\Context as OriginalContext;
14
14
  /**
15
15
  * @extends <% if (modelClassName) { %>Resource\AbstractDatabaseResource<<%= modelClassName %>><% } else { %>Resource\AbstractResource<object><% } %>
16
16
  */
17
- class <%= className %> extends <% if (modelClassName) { %>Resource\AbstractDatabaseResource<% } else { %>Resource\AbstractResource<% if (interfaces.length > 0) %> implements <%= interfaces.join(', ') %><% } %>
17
+ class <%= className %> extends <% if (modelClassName) { %>Resource\AbstractDatabaseResource<% } else { %>Resource\AbstractResource<% if (interfaces.length > 0) { %> implements <%= interfaces.join(', ') %><% } %><% } %>
18
18
  {
19
19
  public function type(): string
20
20
  {
@@ -35,7 +35,7 @@ class <%= className %> extends <% if (modelClassName) { %>Resource\AbstractDatab
35
35
  {
36
36
  return [<% if (endpoints.includes('create')) { %>
37
37
  Endpoint\Create::make()<% if (modelClassName) { %>
38
- ->can('create<%= modelClassName %>'),<% } %><% } %><% if (endpoints.includes('update')) { %>
38
+ ->can('create<%= modelClassName %>')<% } %>,<% } %><% if (endpoints.includes('update')) { %>
39
39
  Endpoint\Update::make()
40
40
  ->can('update'),<% } %><% if (endpoints.includes('delete')) { %>
41
41
  Endpoint\Delete::make()
@@ -52,7 +52,7 @@ class <%= className %> extends <% if (modelClassName) { %>Resource\AbstractDatab
52
52
  return [
53
53
 
54
54
  /**
55
- * @todo migrate logic from old serializer and controllers to this API Resource.
55
+ * @TODO migrate logic from old serializer and controllers to this API Resource.
56
56
  * @see https://docs.flarum.org/2.x/extend/api#api-resources
57
57
  */
58
58
 
@@ -0,0 +1,17 @@
1
+ <?php
2
+
3
+ namespace <%= classNamespace %>;
4
+
5
+ use Illuminate\Database\Eloquent\Factories\Factory;
6
+
7
+ class <%= className %> extends Factory
8
+ {
9
+ // A default for each column, so tests only need to set the ones they care about.
10
+ // See https://docs.flarum.org/2.x/extend/testing#model-factories for more information.
11
+ public function definition(): array
12
+ {
13
+ return [
14
+ //
15
+ ];
16
+ }
17
+ }
@@ -5,9 +5,12 @@ namespace <%= classNamespace %>;
5
5
  use Flarum\Database\AbstractModel;
6
6
  use Flarum\Database\ScopeVisibilityTrait;
7
7
  use Flarum\Foundation\EventGeneratorTrait;
8
+ use Illuminate\Database\Eloquent\Factories\HasFactory;
8
9
 
9
10
  class <%= className %> extends AbstractModel
10
11
  {
12
+ use HasFactory;
13
+
11
14
  // See https://docs.flarum.org/2.x/extend/models.html#backend-models for more information.
12
15
  <% if (tableName) { %>
13
16
  protected $table = '<%= tableName %>';<% } %>
@@ -1,5 +1,5 @@
1
1
  import { Command, Interfaces } from '@oclif/core';
2
- import { IO } from 'boilersmith/io';
2
+ import { IO, Message } from 'boilersmith/io';
3
3
  import { StepManager } from 'boilersmith/step-manager';
4
4
  import { FlarumProviders } from './providers';
5
5
  export declare enum LocationType {
@@ -20,19 +20,37 @@ export default abstract class BaseCommand extends Command {
20
20
  protected skipFinalMessage: boolean;
21
21
  runSteps(steps: StepManager<FlarumProviders>): Promise<{
22
22
  succeeded: true;
23
- messages: import("boilersmith/io").Message[];
23
+ messages: Message[];
24
24
  stepsRan: string[];
25
25
  } | {
26
26
  succeeded: false;
27
27
  error: string;
28
28
  errorTrace?: string | undefined;
29
- messages: import("boilersmith/io").Message[];
29
+ messages: Message[];
30
30
  stepsRan: string[];
31
31
  e?: any;
32
32
  }>;
33
+ /**
34
+ * Where the steps run, which is where the command was asked to unless a
35
+ * command runs them somewhere else.
36
+ */
37
+ protected stepsPath(path: string | undefined): string | undefined;
33
38
  extRoot(path: string | undefined): Promise<string>;
34
39
  run(): Promise<void>;
35
40
  protected welcomeMessage(): string;
41
+ /**
42
+ * The result of a run with `--json`, for the program that started it.
43
+ */
44
+ protected jsonReport(succeeded: boolean, error: string | null, messages: Message[]): Record<string, unknown>;
45
+ /**
46
+ * Written by hand rather than returned from `run()`: oclif colours JSON it
47
+ * prints itself whenever the output is a terminal.
48
+ */
49
+ protected writeJson(report: Record<string, unknown>): void;
50
+ /**
51
+ * With `--json`, an error before or between steps is still reported as JSON.
52
+ */
53
+ catch(err: any): Promise<any>;
36
54
  protected goodbyeMessage(): string;
37
55
  protected additionalPreRunChecks(_extRoot: string): Promise<void>;
38
56
  protected abstract steps(stepManager: StepManager<FlarumProviders>, extRoot: string): StepManager<FlarumProviders>;
@@ -16,6 +16,7 @@ const chalk_1 = tslib_1.__importDefault(require("chalk"));
16
16
  const errors_1 = require("@oclif/errors");
17
17
  const monorepo_1 = require("./utils/monorepo");
18
18
  const mem_fs_1 = require("mem-fs");
19
+ const terminal_1 = require("./utils/terminal");
19
20
  var LocationType;
20
21
  (function (LocationType) {
21
22
  LocationType[LocationType["FLARUM_EXTENSION"] = 0] = "FLARUM_EXTENSION";
@@ -34,7 +35,7 @@ class BaseCommand extends core_1.Command {
34
35
  async runSteps(steps) {
35
36
  const { args } = await this.parse(this.constructor);
36
37
  this.args = args;
37
- const path = args.path;
38
+ const path = this.stepsPath(args.path);
38
39
  const paths = new paths_1.NodePaths({
39
40
  requestedDir: path,
40
41
  package: await this.extRoot(path),
@@ -42,6 +43,13 @@ class BaseCommand extends core_1.Command {
42
43
  const phpProvider = new php_provider_1.PhpSubsystemProvider((0, path_1.resolve)(__dirname, '../php-subsystem/index.php'));
43
44
  return steps.run(paths, this.genIO(), { php: phpProvider }, this.dry, args);
44
45
  }
46
+ /**
47
+ * Where the steps run, which is where the command was asked to unless a
48
+ * command runs them somewhere else.
49
+ */
50
+ stepsPath(path) {
51
+ return path;
52
+ }
45
53
  async extRoot(path) {
46
54
  let extRoot;
47
55
  if (this.requireExistingExtension) {
@@ -56,7 +64,7 @@ class BaseCommand extends core_1.Command {
56
64
  return extRoot;
57
65
  }
58
66
  async run() {
59
- var _a;
67
+ var _a, _b, _c;
60
68
  const { args, flags } = await this.parse(this.constructor);
61
69
  this.args = args;
62
70
  this.flags = flags;
@@ -70,6 +78,14 @@ class BaseCommand extends core_1.Command {
70
78
  await this.additionalPreRunChecks(extRoot);
71
79
  const out = await this.runSteps(this.steps(new step_manager_1.StepManager(), extRoot));
72
80
  const errorMessages = out.messages.filter((m) => m.type === 'error');
81
+ if (this.jsonEnabled()) {
82
+ const succeeded = out.succeeded && errorMessages.length === 0;
83
+ const error = out.succeeded ? (_b = (_a = errorMessages[0]) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : null : out.error;
84
+ this.writeJson(this.jsonReport(succeeded, error, out.messages));
85
+ if (!succeeded)
86
+ process.exitCode = 1;
87
+ return;
88
+ }
73
89
  // this.log('\n\n');
74
90
  if (!this.skipFinalMessage && out.succeeded && errorMessages.length === 0) {
75
91
  this.log(chalk_1.default.bold(chalk_1.default.underline(chalk_1.default.green('Success! The following steps were completed:'))));
@@ -88,12 +104,12 @@ class BaseCommand extends core_1.Command {
88
104
  }
89
105
  }
90
106
  else if (!out.succeeded) {
91
- const prettyPrint = ((_a = out.e) === null || _a === void 0 ? void 0 : _a.code) === 'FL_ERR';
107
+ const prettyPrint = ((_c = out.e) === null || _c === void 0 ? void 0 : _c.code) === 'FL_ERR';
92
108
  if (!prettyPrint)
93
109
  this.log(chalk_1.default.bold(chalk_1.default.underline(chalk_1.default.red('Error occurred, and could not complete:'))));
94
110
  if (prettyPrint) {
95
111
  this.log('\n');
96
- this.log('\u001B[A => ' + chalk_1.default.bgRed.bold(' ERROR ') + ' ' + chalk_1.default.red(out.error));
112
+ this.log((0, terminal_1.overwrite)() + ' => ' + chalk_1.default.bgRed.bold(' ERROR ') + ' ' + chalk_1.default.red(out.error));
97
113
  }
98
114
  else {
99
115
  this.log(chalk_1.default.red(out.error));
@@ -130,6 +146,36 @@ class BaseCommand extends core_1.Command {
130
146
  welcomeMessage() {
131
147
  return '';
132
148
  }
149
+ /**
150
+ * The result of a run with `--json`, for the program that started it.
151
+ */
152
+ jsonReport(succeeded, error, messages) {
153
+ return {
154
+ status: succeeded ? 'succeeded' : 'failed',
155
+ error: error === null ? null : (0, terminal_1.stripAnsi)(error),
156
+ messages: messages.map((m) => (Object.assign(Object.assign({}, m), { message: (0, terminal_1.stripAnsi)(m.message) }))),
157
+ };
158
+ }
159
+ /**
160
+ * Written by hand rather than returned from `run()`: oclif colours JSON it
161
+ * prints itself whenever the output is a terminal.
162
+ */
163
+ writeJson(report) {
164
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
165
+ }
166
+ /**
167
+ * With `--json`, an error before or between steps is still reported as JSON.
168
+ */
169
+ async catch(err) {
170
+ var _a, _b, _c;
171
+ if (!this.jsonEnabled())
172
+ return super.catch(err);
173
+ const code = (_b = (_a = err === null || err === void 0 ? void 0 : err.oclif) === null || _a === void 0 ? void 0 : _a.exit) !== null && _b !== void 0 ? _b : 1;
174
+ if (code === 0)
175
+ return;
176
+ process.exitCode = code;
177
+ this.writeJson(this.jsonReport(false, (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : String(err), []));
178
+ }
133
179
  goodbyeMessage() {
134
180
  return 'Please make sure to check my work, adjust formatting, and test before committing!!!';
135
181
  }
@@ -140,7 +186,8 @@ class BaseCommand extends core_1.Command {
140
186
  // Confirmation
141
187
  // ----------------------------------------------------------------
142
188
  genIO() {
143
- return new io_1.PromptsIO(this.args, [], this.flags['no-interaction'], errors_1.exit);
189
+ // Nobody can answer a prompt when a program is reading the output.
190
+ return new io_1.PromptsIO(this.args, [], this.flags['no-interaction'] || this.jsonEnabled(), errors_1.exit);
144
191
  }
145
192
  monorepoPaths(options) {
146
193
  var _a, _b;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const base_command_1 = tslib_1.__importDefault(require("../../../base-command"));
5
5
  const model_1 = require("../../../steps/stubs/backend/model");
6
+ const model_factory_1 = require("../../../steps/stubs/backend/model-factory");
6
7
  const migration_1 = require("../../../steps/stubs/backend/migration");
7
8
  const api_resource_1 = require("../../../steps/stubs/backend/api-resource");
8
9
  const policy_1 = require("../../../steps/stubs/backend/policy");
@@ -16,8 +17,17 @@ class BackendModel extends base_command_1.default {
16
17
  });
17
18
  }
18
19
  static steps(stepManager, STUB_PATH, shouldRun, dependencies, predefinedDependencies) {
20
+ // The factory is not optional: the model uses HasFactory, which needs it.
19
21
  return stepManager
20
22
  .namedStep('backendModel', new model_1.GenerateModelStub(STUB_PATH, (0, gen_ext_scaffolder_1.genExtScaffolder)()), shouldRun, dependencies, predefinedDependencies)
23
+ .step(new model_factory_1.GenerateModelFactoryStub(STUB_PATH, (0, gen_ext_scaffolder_1.genExtScaffolder)()), {}, [
24
+ {
25
+ sourceStep: 'backendModel',
26
+ exposedName: 'className',
27
+ consumedName: 'className',
28
+ modifier: (modelClassName) => `${modelClassName}Factory`,
29
+ },
30
+ ])
21
31
  .step(new migration_1.GenerateMigrationStub(STUB_PATH, (0, gen_ext_scaffolder_1.genExtScaffolder)()), { optional: true, confirmationMessage: 'Generate corresponding Migration?', default: true }, [
22
32
  {
23
33
  sourceStep: 'backendModel',
@@ -1,17 +1,60 @@
1
1
  import { StepManager } from 'boilersmith/step-manager';
2
2
  import BaseCommand from '../../base-command';
3
3
  import { FlarumProviders } from '../../providers';
4
+ import { BaseUpgradeStep } from '../../steps/upgrade/twopointoh/base';
5
+ import { IO, Message } from 'boilersmith/io';
6
+ /**
7
+ * The temporary worktree a check runs the steps in, so the extension itself
8
+ * is never changed.
9
+ */
10
+ type Checkout = {
11
+ /** The repository the extension is in. */
12
+ repo: string;
13
+ worktree: string;
14
+ branch: string;
15
+ /** The extension's root inside the worktree. */
16
+ root: string;
17
+ /** What was checked. */
18
+ commit: string;
19
+ /** Whether the extension had changes not committed, which a check leaves out. */
20
+ dirty: boolean;
21
+ cwd: string;
22
+ };
4
23
  export default class TwoPointOh extends BaseCommand {
5
24
  static description: string;
25
+ static enableJsonFlag: boolean;
6
26
  static flags: {
27
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
28
  step: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined>;
8
29
  'force-steps': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
30
+ check: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
31
  };
10
32
  static args: import("@oclif/core/lib/interfaces").Arg<any>[];
11
33
  protected requireExistingExtension: boolean;
12
34
  skipFinalMessage: boolean;
35
+ /**
36
+ * The steps this run was asked to take, in order.
37
+ */
38
+ protected scheduled: BaseUpgradeStep[];
39
+ protected checkout: Checkout | null;
40
+ run(): Promise<void>;
41
+ protected additionalPreRunChecks(extRoot: string): Promise<void>;
42
+ protected stepsPath(requested: string | undefined): string | undefined;
43
+ protected genIO(): IO;
44
+ protected createCheckout(extRoot: string): Promise<Checkout>;
45
+ /** Remove the worktree and its branch, saying so if it can't. */
46
+ protected removeCheckout(): Promise<void>;
47
+ /** What a step that committed in the worktree would change, from its commit. */
48
+ protected wouldChange(sha: string): {
49
+ files: string[];
50
+ diff: string;
51
+ };
13
52
  protected steps(steps: StepManager<FlarumProviders>, extRoot: string): StepManager<FlarumProviders>;
14
53
  private prepareSteps;
54
+ protected jsonReport(succeeded: boolean, error: string | null, messages: Message[]): Record<string, unknown>;
55
+ /** A step's outcome, as what it would change when checking. */
56
+ protected reported(step: BaseUpgradeStep): Record<string, unknown>;
15
57
  protected welcomeMessage(): string;
16
58
  protected goodbyeMessage(): string;
17
59
  }
60
+ export {};
@@ -8,6 +8,7 @@ const import_ext_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopoi
8
8
  const compat_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/frontend/export-registry/compat"));
9
9
  const usage_of_lazy_modules_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/frontend/code-splitting/usage-of-lazy-modules"));
10
10
  const misc_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/frontend/misc"));
11
+ const admin_extender_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/frontend/admin-extender"));
11
12
  const format_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/frontend/format"));
12
13
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
13
14
  const misc_2 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/backend/misc"));
@@ -17,14 +18,90 @@ const json_api_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopoint
17
18
  const search_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/backend/search"));
18
19
  const less_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/less"));
19
20
  const phpunit_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/backend/phpunit"));
21
+ const model_factories_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/backend/model-factories"));
20
22
  const core_1 = require("@oclif/core");
21
23
  const email_views_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/backend/email-views"));
22
24
  const intervention_image_1 = tslib_1.__importDefault(require("../../steps/upgrade/twopointoh/backend/intervention-image"));
25
+ const child_process_1 = require("child_process");
26
+ const fs_1 = tslib_1.__importDefault(require("fs"));
27
+ const os_1 = tslib_1.__importDefault(require("os"));
28
+ const path_1 = tslib_1.__importDefault(require("path"));
29
+ const simple_git_1 = tslib_1.__importDefault(require("simple-git"));
23
30
  class TwoPointOh extends base_command_1.default {
24
31
  constructor() {
25
32
  super(...arguments);
26
33
  this.requireExistingExtension = true;
27
34
  this.skipFinalMessage = true;
35
+ /**
36
+ * The steps this run was asked to take, in order.
37
+ */
38
+ this.scheduled = [];
39
+ this.checkout = null;
40
+ }
41
+ async run() {
42
+ try {
43
+ await super.run();
44
+ }
45
+ finally {
46
+ await this.removeCheckout();
47
+ }
48
+ }
49
+ async additionalPreRunChecks(extRoot) {
50
+ if (this.flags.check) {
51
+ this.checkout = await this.createCheckout(extRoot);
52
+ }
53
+ }
54
+ stepsPath(requested) {
55
+ var _a, _b;
56
+ return (_b = (_a = this.checkout) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : requested;
57
+ }
58
+ genIO() {
59
+ var _a;
60
+ // A check asks nothing: there is nothing to confirm.
61
+ if ((_a = this.flags) === null || _a === void 0 ? void 0 : _a.check)
62
+ this.flags['no-interaction'] = true;
63
+ return super.genIO();
64
+ }
65
+ async createCheckout(extRoot) {
66
+ const root = fs_1.default.realpathSync(extRoot);
67
+ const git = (0, simple_git_1.default)(root);
68
+ let repo;
69
+ try {
70
+ repo = (await git.revparse(['--show-toplevel'])).trim();
71
+ }
72
+ catch (_a) {
73
+ this.error('--check runs the upgrade on a copy of the extension in a git worktree, so the extension must be in a git repository.', {
74
+ code: 'FL_ERR',
75
+ });
76
+ }
77
+ const commit = (await git.revparse(['HEAD'])).trim();
78
+ const dirty = !(await git.status()).isClean();
79
+ const worktree = fs_1.default.mkdtempSync(path_1.default.join(fs_1.default.realpathSync(os_1.default.tmpdir()), 'flarum-upgrade-check-'));
80
+ const branch = `flarum-upgrade-check/${path_1.default.basename(worktree)}`;
81
+ await (0, simple_git_1.default)(repo).raw(['worktree', 'add', '--quiet', '-b', branch, worktree, commit]);
82
+ return { repo, worktree, branch, root: path_1.default.join(worktree, path_1.default.relative(repo, root)), commit, dirty, cwd: process.cwd() };
83
+ }
84
+ /** Remove the worktree and its branch, saying so if it can't. */
85
+ async removeCheckout() {
86
+ const checkout = this.checkout;
87
+ if (!checkout)
88
+ return;
89
+ this.checkout = null;
90
+ // The steps ran from inside the worktree.
91
+ process.chdir(checkout.cwd);
92
+ try {
93
+ await (0, simple_git_1.default)(checkout.repo).raw(['worktree', 'remove', '--force', checkout.worktree]);
94
+ await (0, simple_git_1.default)(checkout.repo).raw(['branch', '-D', checkout.branch]);
95
+ }
96
+ catch (error) {
97
+ process.stderr.write(`The check could not remove its worktree ${checkout.worktree} and branch ${checkout.branch}: ${error.message}\n` +
98
+ `Remove them with: git worktree remove --force ${checkout.worktree} && git branch -D ${checkout.branch}\n`);
99
+ }
100
+ }
101
+ /** What a step that committed in the worktree would change, from its commit. */
102
+ wouldChange(sha) {
103
+ const show = (...args) => (0, child_process_1.execFileSync)('git', ['show', '--format=', '--no-color', '--no-ext-diff', ...args, sha], { cwd: this.checkout.worktree, encoding: 'utf8' });
104
+ return { files: show('--name-only').split('\n').filter(Boolean), diff: show() };
28
105
  }
29
106
  steps(steps, extRoot) {
30
107
  // Error.stackTraceLimit = Number.POSITIVE_INFINITY;
@@ -37,6 +114,7 @@ class TwoPointOh extends base_command_1.default {
37
114
  import_ext_1.default,
38
115
  usage_of_lazy_modules_1.default,
39
116
  misc_1.default,
117
+ admin_extender_1.default,
40
118
  // Backend
41
119
  misc_2.default,
42
120
  filesystem_1.default,
@@ -45,6 +123,7 @@ class TwoPointOh extends base_command_1.default {
45
123
  json_api_1.default,
46
124
  intervention_image_1.default,
47
125
  search_1.default,
126
+ model_factories_1.default,
48
127
  phpunit_1.default,
49
128
  // LESS
50
129
  less_1.default,
@@ -57,23 +136,44 @@ class TwoPointOh extends base_command_1.default {
57
136
  total++;
58
137
  });
59
138
  collection = collection.map((Step) => {
60
- const step = new Step(this, extRoot, stepCount, total, this.flags['force-steps']);
139
+ var _a, _b;
140
+ const step = new Step(this, (_b = (_a = this.checkout) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : extRoot, stepCount, total, this.flags['force-steps'] || this.flags.check);
141
+ step.checking = Boolean(this.flags.check);
61
142
  stepCount++;
62
143
  return step;
63
144
  });
64
145
  if (this.flags.step) {
65
146
  const step = Number.parseInt(this.flags.step, 10);
66
147
  if (step > 0 && step <= total) {
148
+ this.scheduled = [collection[step - 1]];
67
149
  return steps.step(collection[step - 1]);
68
150
  }
69
151
  this.error(`Invalid step number. Please provide a number between 1 and ${total}`);
70
152
  }
153
+ this.scheduled = collection;
71
154
  collection.forEach((step) => {
72
155
  steps.step(step);
73
156
  });
74
157
  return steps;
75
158
  }
159
+ jsonReport(succeeded, error, messages) {
160
+ return Object.assign(Object.assign(Object.assign({}, super.jsonReport(succeeded, error, messages)), (this.checkout && { check: { commit: this.checkout.commit, uncommittedChangesLeftOut: this.checkout.dirty } })), { steps: this.scheduled.map((step) => (Object.assign({ step: step.number, title: step.type }, this.reported(step)))) });
161
+ }
162
+ /** A step's outcome, as what it would change when checking. */
163
+ reported(step) {
164
+ var _a;
165
+ const outcome = (_a = step.outcome) !== null && _a !== void 0 ? _a : { status: 'not-run' };
166
+ if (!this.checkout || outcome.status !== 'committed')
167
+ return outcome;
168
+ return Object.assign(Object.assign({ status: 'would-change' }, this.wouldChange(outcome.commit.sha)), { notes: outcome.notes, todos: outcome.todos });
169
+ }
76
170
  welcomeMessage() {
171
+ if (this.flags.check) {
172
+ return `
173
+ Checking what the Flarum 2.0 upgrade would change in your extension. The steps run on its last commit,
174
+ in a temporary git worktree that is removed afterwards: nothing in your extension changes.
175
+ `;
176
+ }
77
177
  const beforeProceeding = chalk_1.default.bold.red('Before proceeding:');
78
178
  const command = chalk_1.default.bold.bgYellow.black('fl update js-imports');
79
179
  const makeSure = chalk_1.default.bold.dim(`- Make sure your extend.php file directly returns an array of extenders.
@@ -90,6 +190,25 @@ class TwoPointOh extends base_command_1.default {
90
190
  `;
91
191
  }
92
192
  goodbyeMessage() {
193
+ if (this.checkout) {
194
+ const changing = this.scheduled.filter((step) => { var _a; return ((_a = step.outcome) === null || _a === void 0 ? void 0 : _a.status) === 'committed'; });
195
+ const lines = changing.map((step) => {
196
+ const { files } = this.wouldChange(step.outcome.commit.sha);
197
+ return ` - Step ${step.number}, ${step.type} ${chalk_1.default.dim(files.join(', '))}`;
198
+ });
199
+ return [
200
+ changing.length > 0
201
+ ? ` ${changing.length} of the steps would change something in ${this.checkout.commit.slice(0, 7)}:`
202
+ : ` None of the steps would change anything in ${this.checkout.commit.slice(0, 7)}.`,
203
+ ...lines,
204
+ this.checkout.dirty && ' Changes that were not committed were left out of the check.',
205
+ '',
206
+ ' Nothing was changed. Run with --json as well to see each diff, or without --check to make the changes.',
207
+ '',
208
+ ]
209
+ .filter((line) => line !== false)
210
+ .join('\n');
211
+ }
93
212
  const composer = chalk_1.default.bold.bgYellow('composer update');
94
213
  const yarn = chalk_1.default.bold.bgYellow('yarn install');
95
214
  return ` Your extension has been successfully upgraded to Flarum 2.0.
@@ -103,11 +222,18 @@ class TwoPointOh extends base_command_1.default {
103
222
  }
104
223
  exports.default = TwoPointOh;
105
224
  TwoPointOh.description = 'Upgrade an extension to Flarum 2.0';
106
- TwoPointOh.flags = Object.assign(Object.assign({}, base_command_1.default.flags), { step: core_1.Flags.string({
225
+ TwoPointOh.enableJsonFlag = true;
226
+ TwoPointOh.flags = Object.assign(Object.assign({}, base_command_1.default.flags), { json: core_1.Flags.boolean({
227
+ description: "Report each step's outcome, failures, notes and the @TODOs it left as JSON, for a program driving the upgrade. Implies --no-interaction.",
228
+ required: false,
229
+ }), step: core_1.Flags.string({
107
230
  description: 'Optionally specify a specific step to run',
108
231
  required: false,
109
232
  }), 'force-steps': core_1.Flags.boolean({
110
233
  description: 'Force run steps even if already ran',
111
234
  required: false,
235
+ }), check: core_1.Flags.boolean({
236
+ description: 'Report what the upgrade would change, such as on an extension already on Flarum 2.0, without changing it: the steps run on the last commit, in a temporary git worktree that is removed afterwards. Implies --force-steps and --no-interaction.',
237
+ required: false,
112
238
  }) });
113
239
  TwoPointOh.args = [...base_command_1.default.args];
@@ -1,4 +1,9 @@
1
1
  import { Scaffolder } from 'boilersmith/scaffolding/scaffolder';
2
+ /**
3
+ * The SPDX text for a licence, or an empty string when it isn't a known SPDX
4
+ * identifier (e.g. "proprietary" or a free-form "© Some Company").
5
+ */
6
+ export declare function licenseTextFor(licenseType: unknown): string;
2
7
  export declare const EXTENSION_PARAMS: readonly ["packageName", "packageDescription", "packageNamespace", "authorName", "authorEmail", "extensionName", "licenseType", "mainGitBranch", "jsPackageManager", "licenseText", "extensionId", "year", "backendDirectory", "frontendDirectory"];
3
8
  export type ExtensionParams = (typeof EXTENSION_PARAMS)[number];
4
9
  export declare const EXTENSION_MODULES: readonly ["core", "icon", "admin", "forum", "js", "jsCommon", "css", "locale", "gitConf", "githubActions", "prettier", "typescript", "bundlewatch", "frontendTesting", "backendTesting", "phpstan", "editorConfig", "styleci"];