@flarum/cli 3.0.12 → 3.0.14

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 +5 -5
  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 +55 -0
  35. package/lib/steps/upgrade/twopointoh/dependencies.js +317 -20
  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-5-9.d.ts +15 -0
  49. package/lib/steps/upgrade/twopointoh/typescript-5-9.js +250 -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
@@ -3,10 +3,101 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const base_1 = require("./base");
5
5
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
6
+ const fs_1 = tslib_1.__importDefault(require("fs"));
7
+ const path_1 = tslib_1.__importDefault(require("path"));
8
+ const typescript_5_9_1 = require("./typescript-5-9");
9
+ // What `fl init` gave 1.x extensions. `sed -i` with no suffix is GNU sed's
10
+ // form: BSD sed, as on macOS, takes the next argument as the suffix and fails.
11
+ // With the suffix attached, both work.
12
+ const GNU_POST_BUILD_TYPINGS = "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'";
13
+ const PORTABLE_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";
14
+ // Packages an extension only needs to build, check and test its frontend.
15
+ const BUILD_TOOLING = new Set([
16
+ '@flarum/jest-config',
17
+ '@flarum/prettier-config',
18
+ 'flarum-tsconfig',
19
+ 'flarum-webpack-config',
20
+ 'prettier',
21
+ 'typescript',
22
+ 'typescript-coverage-report',
23
+ 'webpack',
24
+ 'webpack-cli',
25
+ ]);
26
+ // The scripts that build and check an extension's typings, as the reusable
27
+ // frontend workflow runs them.
28
+ const TYPINGS_SCRIPTS = ['clean-typings', 'build-typings', 'post-build-typings', 'check-typings', 'check-typings-coverage'];
29
+ // What those scripts run.
30
+ const TYPINGS_TOOLING = ['typescript', 'typescript-coverage-report'];
31
+ /** The package.json `fl init` gives a new extension, for the given package manager. */
32
+ function scaffoldedPackageJson(packageManager) {
33
+ const skeleton = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../../boilerplate/skeleton/extension/js/package.json'), 'utf8');
34
+ // EJS placeholders only appear inside strings, so it parses as JSON.
35
+ return JSON.parse(skeleton.replace(/<%= params\.jsPackageManager %>/g, packageManager));
36
+ }
37
+ const TYPES_COMMENT = [
38
+ 'Only load the type packages the code imports, not every one in',
39
+ 'node_modules, such as the @types/node webpack brings along.',
40
+ ];
41
+ /**
42
+ * The tsconfig.json with an option first in its compilerOptions, and the
43
+ * comment lines above it, indented as the options there are. Null when it has
44
+ * no compilerOptions to put it in.
45
+ */
46
+ function insertCompilerOption(code, comment, option) {
47
+ var _a;
48
+ const open = code.match(/"compilerOptions"\s*:\s*{/);
49
+ if (!open)
50
+ return null;
51
+ const at = open.index + open[0].length;
52
+ const eol = code.includes('\r\n') ? '\r\n' : '\n';
53
+ const next = code.slice(at).match(/\r?\n([\t ]*)\S/);
54
+ const indent = (_a = next === null || next === void 0 ? void 0 : next[1]) !== null && _a !== void 0 ? _a : ' ';
55
+ const lines = [...(comment !== null && comment !== void 0 ? comment : []).map((line) => `// ${line}`), `${option},`];
56
+ return code.slice(0, at) + lines.map((line) => eol + indent + line).join('') + code.slice(at);
57
+ }
58
+ /** Dependencies in the alphabetical order package managers keep them in. */
59
+ const sortedByName = (dependencies) => Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
60
+ /**
61
+ * Whether a TypeScript constraint could resolve to TypeScript 6 or newer:
62
+ * `^6.0.3`, `>=4.2`, `latest`, `*`. A caret, tilde or exact constraint on 4 or
63
+ * 5 can't.
64
+ */
65
+ const allowsSixOrNewer = (constraint) => {
66
+ const match = constraint.trim().match(/^[=^~]?\s*v?(\d+)(?:\.|$)/);
67
+ return !match || Number(match[1]) >= 6;
68
+ };
69
+ /**
70
+ * The lowest version a simple constraint allows, as [major, minor, patch]:
71
+ * `^5.1.4`, `~4.7`, `>=8.3`, `5.x`. Null for anything else, such as `*`, a
72
+ * range with `||` or a prerelease.
73
+ */
74
+ const lowestVersion = (constraint) => {
75
+ const match = constraint.trim().match(/^(?:\^|~|>=|=)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/);
76
+ return match ? match.slice(1).map((part) => (part && /^\d+$/.test(part) ? Number(part) : 0)) : null;
77
+ };
78
+ /** Whether a constraint asks for a newer version than another does. */
79
+ const asksForNewer = (constraint, than) => {
80
+ const asked = lowestVersion(constraint);
81
+ const other = lowestVersion(than);
82
+ if (!asked || !other)
83
+ return false;
84
+ const differs = asked.findIndex((part, i) => part !== other[i]);
85
+ return differs !== -1 && asked[differs] > other[differs];
86
+ };
6
87
  class Dependencies extends base_1.BaseUpgradeStep {
7
88
  constructor() {
8
89
  super(...arguments);
9
90
  this.type = 'Update the extension dependencies to the latest versions.';
91
+ /** The TypeScript the extension asked for, when it allowed TypeScript 6 or newer. */
92
+ this.typeScriptWas = null;
93
+ /** What was removed from tsconfig.json, as TypeScript 5.9 refuses it. */
94
+ this.removedCompilerOptions = [];
95
+ /** Constraints left as they were, as they already ask for newer versions. */
96
+ this.keptNewer = [];
97
+ /** Tooling moved from dependencies to devDependencies. */
98
+ this.movedToDev = [];
99
+ /** The typings scripts, and the tooling they run, added to package.json. */
100
+ this.addedTypings = [];
10
101
  }
11
102
  replacements(file) {
12
103
  const replacements = [];
@@ -16,11 +107,18 @@ class Dependencies extends base_1.BaseUpgradeStep {
16
107
  break;
17
108
  case 'js/package.json':
18
109
  replacements.push(this.updatePackageJson());
110
+ break;
111
+ case 'js/tsconfig.json':
112
+ replacements.push(this.updateTsConfig(), this.limitTypes());
113
+ break;
114
+ case 'js/tsconfig.test.json':
115
+ replacements.push(this.testTypes());
116
+ break;
19
117
  }
20
118
  return replacements;
21
119
  }
22
120
  targets() {
23
- return ['composer.json', 'js/package.json'];
121
+ return ['composer.json', 'js/package.json', 'js/tsconfig.json', 'js/tsconfig.test.json'];
24
122
  }
25
123
  gitCommit() {
26
124
  return {
@@ -33,9 +131,33 @@ class Dependencies extends base_1.BaseUpgradeStep {
33
131
  const composerUpdate = chalk_1.default.bgYellowBright('composer update');
34
132
  const yarn = chalk_1.default.bgYellowBright('yarn');
35
133
  const npm = chalk_1.default.bgYellowBright('npm install');
36
- return `Dependencies updated. Please review the changes and update any other dependency versions as needed.
37
- It is recommended to run ${composerInstall} or ${composerUpdate} and ${yarn} or ${npm} now.
38
- But you may also leave that until the upgrade process is over.`;
134
+ const notes = [
135
+ this.typeScriptWas &&
136
+ `TypeScript ${this.typeScriptWas} was brought down to ^5.9.3: flarum-tsconfig resolves modules the way TypeScript 6 refuses without ignoreDeprecations.`,
137
+ this.removedCompilerOptions.length > 0 &&
138
+ `Removed from js/tsconfig.json, as TypeScript 5.9 refuses them: ${this.removedCompilerOptions.join(', ')}.`,
139
+ this.addedTypings.length > 0 && `Added what the reusable frontend workflow runs to build and check typings: ${this.addedTypings.join(', ')}.`,
140
+ this.movedToDev.length > 0 && `Moved to devDependencies, as the extension only builds with them: ${this.movedToDev.join(', ')}.`,
141
+ this.keptNewer.length > 0 && `Kept, as they already ask for newer versions than Flarum 2.0 needs: ${this.keptNewer.join(', ')}.`,
142
+ ].filter(Boolean);
143
+ return [
144
+ 'Dependencies updated. Please review the changes and update any other dependency versions as needed.',
145
+ ...notes,
146
+ `It is recommended to run ${composerInstall} or ${composerUpdate} and ${yarn} or ${npm} now.`,
147
+ 'But you may also leave that until the upgrade process is over.',
148
+ ].join('\n ');
149
+ }
150
+ /**
151
+ * Require at least the given version, leaving a constraint that already
152
+ * asks for a newer one as it is.
153
+ */
154
+ raise(dependencies, name, version) {
155
+ if (asksForNewer(String(dependencies[name]), version)) {
156
+ this.keptNewer.push(`${name} ${dependencies[name]}`);
157
+ }
158
+ else {
159
+ dependencies[name] = version;
160
+ }
39
161
  }
40
162
  updateComposerJson() {
41
163
  return (_file, _code, advanced) => {
@@ -52,7 +174,7 @@ class Dependencies extends base_1.BaseUpgradeStep {
52
174
  }
53
175
  });
54
176
  if (key === 'flarum/core') {
55
- composer.require[key] = '^2.0.0';
177
+ this.raise(composer.require, key, '^2.0.0');
56
178
  }
57
179
  else if (key.startsWith('flarum/') && value !== '*') {
58
180
  composer.require[key] = '*';
@@ -67,22 +189,20 @@ class Dependencies extends base_1.BaseUpgradeStep {
67
189
  }
68
190
  });
69
191
  if (['flarum/core', 'flarum/testing', 'flarum/phpstan'].includes(key)) {
70
- composer['require-dev'][key] = '^2.0.0';
192
+ this.raise(composer['require-dev'], key, '^2.0.0');
71
193
  }
72
194
  else if (key.startsWith('flarum/') && value.startsWith('^1')) {
73
195
  composer['require-dev'][key] = '*';
74
196
  }
75
197
  }
198
+ // An extension using newer PHP than Flarum 2.0 needs keeps asking for it.
76
199
  if (composer.require.php) {
77
- composer.require.php = '^8.3';
200
+ this.raise(composer.require, 'php', '^8.3');
78
201
  }
79
- // 2.0 constraints resolve to prereleases while the line is in RC, so the
80
- // extension has to accept them — with stable preferred wherever one
81
- // exists, so only Flarum itself comes from the beta channel.
82
- composer['minimum-stability'] = 'beta';
83
- composer['prefer-stable'] = true;
202
+ // Flarum 2.0 is stable, so its constraints resolve without changing the
203
+ // extension's minimum stability.
84
204
  if (composer.require['fof/extend']) {
85
- composer.require['fof/extend'] = '^2.0.0';
205
+ this.raise(composer.require, 'fof/extend', '^2.0.0');
86
206
  }
87
207
  return {
88
208
  updated: composer,
@@ -91,20 +211,47 @@ class Dependencies extends base_1.BaseUpgradeStep {
91
211
  }
92
212
  updatePackageJson() {
93
213
  return (_file, _code, advanced) => {
214
+ var _a;
94
215
  advanced = advanced;
216
+ // The versions a new extension is scaffolded with. 3.1.0 is the first
217
+ // flarum-webpack-config that supplies TypeScript 5.9 and only asks for
218
+ // webpack as a peer, and @flarum/jest-config 2.1.0 tests with the mithril
219
+ // Flarum ships.
95
220
  const map = {
96
- 'flarum-webpack-config': '^3.0.0',
221
+ 'flarum-webpack-config': '^3.1.0',
97
222
  'flarum-tsconfig': '^2.0.0',
98
- '@flarum/jest-config': '^2.0.0',
99
- webpack: '^5.65.0',
223
+ '@flarum/jest-config': '^2.1.0',
224
+ webpack: '^5.104.1',
100
225
  'webpack-cli': '^4.9.1',
226
+ // What Flarum builds and checks its typings with.
227
+ typescript: '^5.9.3',
101
228
  };
102
- const dependencies = advanced.dependencies || advanced.devDependencies;
103
- for (const [key] of Object.entries(dependencies)) {
104
- if (map[key]) {
105
- dependencies[key] = map[key];
229
+ // Extensions list the tooling under either key, often split between both.
230
+ for (const dependencies of [advanced.dependencies, advanced.devDependencies]) {
231
+ for (const key of Object.keys(dependencies || {})) {
232
+ // TypeScript is the exception: it moves to what Flarum checks typings
233
+ // with, whatever the extension asked for. flarum-tsconfig resolves
234
+ // modules the way TypeScript 6 refuses, so a newer one comes down,
235
+ // unless the extension's tsconfig already resolves them another way.
236
+ if (key === 'typescript' && allowsSixOrNewer(String(dependencies[key])) && this.tsConfigReadsWithSixOrNewer()) {
237
+ this.keptNewer.push(`${key} ${dependencies[key]}`);
238
+ }
239
+ else if (key === 'typescript') {
240
+ if (allowsSixOrNewer(String(dependencies[key]))) {
241
+ this.typeScriptWas = dependencies[key];
242
+ }
243
+ dependencies[key] = map[key];
244
+ }
245
+ else if (map[key]) {
246
+ this.raise(dependencies, key, map[key]);
247
+ }
106
248
  }
107
249
  }
250
+ if (((_a = advanced.scripts) === null || _a === void 0 ? void 0 : _a['post-build-typings']) === GNU_POST_BUILD_TYPINGS) {
251
+ advanced.scripts['post-build-typings'] = PORTABLE_POST_BUILD_TYPINGS;
252
+ }
253
+ this.addTypingsScripts(advanced);
254
+ this.moveToolingToDevDependencies(advanced);
108
255
  // Remove duplicates from devDependencies
109
256
  if (advanced.dependencies && advanced.devDependencies) {
110
257
  for (const [key] of Object.entries(advanced.dependencies)) {
@@ -118,5 +265,155 @@ class Dependencies extends base_1.BaseUpgradeStep {
118
265
  };
119
266
  };
120
267
  }
268
+ /**
269
+ * Add the scripts that build and check typings, which a new extension is
270
+ * scaffolded with and the reusable frontend workflow runs, along with the
271
+ * tooling they need. Scripts the extension already has are left alone.
272
+ */
273
+ addTypingsScripts(pkg) {
274
+ const missing = TYPINGS_SCRIPTS.filter((name) => { var _a; return !((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a[name]); });
275
+ if (missing.length === 0)
276
+ return;
277
+ if (!this.hasTsConfig()) {
278
+ this.leaveTodo('js/tsconfig.json', 'Add a tsconfig.json, then the scripts that build and check typings, which the reusable frontend workflow runs: ' + missing.join(', ') + '.');
279
+ return;
280
+ }
281
+ const scaffolded = scaffoldedPackageJson(this.packageManager());
282
+ pkg.scripts = Object.assign({}, pkg.scripts);
283
+ for (const name of missing) {
284
+ pkg.scripts[name] = scaffolded.scripts[name];
285
+ }
286
+ const listed = Object.assign(Object.assign({}, pkg.dependencies), pkg.devDependencies);
287
+ const tooling = TYPINGS_TOOLING.filter((name) => !listed[name]);
288
+ if (tooling.length > 0) {
289
+ pkg.devDependencies = sortedByName(Object.assign(Object.assign({}, pkg.devDependencies), Object.fromEntries(tooling.map((name) => [name, scaffolded.devDependencies[name]]))));
290
+ }
291
+ this.addedTypings.push(...missing, ...tooling);
292
+ }
293
+ hasTsConfig() {
294
+ return fs_1.default.existsSync(path_1.default.join(this.root, 'js/tsconfig.json'));
295
+ }
296
+ /** The package manager the extension's frontend uses, by its lockfile. */
297
+ packageManager() {
298
+ return fs_1.default.existsSync(path_1.default.join(this.root, 'js/yarn.lock')) ? 'yarn' : 'npm';
299
+ }
300
+ /**
301
+ * Flarum 2.0 extensions only build with their tooling: none of it ends up
302
+ * in the bundle, so it belongs in devDependencies. Where the extension lists
303
+ * a tool under both keys, the newer constraint is kept. Other dependencies
304
+ * may be libraries the bundle includes, so they stay where they are.
305
+ */
306
+ moveToolingToDevDependencies(pkg) {
307
+ const moving = Object.keys(pkg.dependencies || {}).filter((name) => BUILD_TOOLING.has(name) || name.startsWith('@types/'));
308
+ if (moving.length === 0)
309
+ return;
310
+ const devDependencies = Object.assign({}, pkg.devDependencies);
311
+ for (const name of moving) {
312
+ if (!devDependencies[name] || asksForNewer(String(pkg.dependencies[name]), String(devDependencies[name]))) {
313
+ devDependencies[name] = pkg.dependencies[name];
314
+ }
315
+ delete pkg.dependencies[name];
316
+ }
317
+ pkg.devDependencies = sortedByName(devDependencies);
318
+ this.movedToDev.push(...moving);
319
+ }
320
+ /**
321
+ * Drop the compiler options, and option values, TypeScript 5.9 refuses: it
322
+ * won't read a tsconfig.json with any. A tsconfig.json it can read is left
323
+ * exactly as it was.
324
+ */
325
+ updateTsConfig() {
326
+ return (_file, _code, advanced) => {
327
+ const tsconfig = advanced;
328
+ const options = tsconfig.compilerOptions || {};
329
+ const removed = [];
330
+ for (const [name, value] of Object.entries(options)) {
331
+ const known = Object.prototype.hasOwnProperty.call(typescript_5_9_1.compilerOptions, name) ? typescript_5_9_1.compilerOptions[name] : null;
332
+ if (!known) {
333
+ removed.push(name);
334
+ delete options[name];
335
+ }
336
+ else if (name === 'lib' && Array.isArray(value)) {
337
+ const kept = value.filter((lib) => typeof lib === 'string' && typescript_5_9_1.libs.includes(lib.toLowerCase()));
338
+ for (const lib of value.filter((lib) => !kept.includes(lib))) {
339
+ removed.push(`lib "${lib}"`);
340
+ }
341
+ if (kept.length === 0) {
342
+ delete options.lib;
343
+ }
344
+ else if (kept.length < value.length) {
345
+ options.lib = kept;
346
+ }
347
+ }
348
+ else if (Array.isArray(known) && typeof value === 'string' && !known.includes(value.toLowerCase())) {
349
+ removed.push(`${name} "${value}"`);
350
+ delete options[name];
351
+ }
352
+ }
353
+ this.removedCompilerOptions.push(...removed);
354
+ return removed.length > 0 ? { updated: tsconfig } : null;
355
+ };
356
+ }
357
+ /**
358
+ * TypeScript loads every type package in node_modules unless told which,
359
+ * including the @types/node that webpack's minimizer brings along. Load only
360
+ * the ones the code imports. The file is edited as text so its comments stay.
361
+ */
362
+ limitTypes() {
363
+ return (_file, code, advanced) => {
364
+ var _a;
365
+ if (((_a = advanced === null || advanced === void 0 ? void 0 : advanced.compilerOptions) === null || _a === void 0 ? void 0 : _a.types) !== undefined)
366
+ return null;
367
+ const updated = insertCompilerOption(code, TYPES_COMMENT, '"types": []');
368
+ if (!updated)
369
+ return null;
370
+ const globals = Object.keys(Object.assign(Object.assign({}, this.packageJson().dependencies), this.packageJson().devDependencies)).filter((name) => name.startsWith('@types/') && name !== '@types/jest');
371
+ if (globals.length > 0) {
372
+ this.leaveTodo('js/tsconfig.json', `It only loads the type packages the code imports now. If the code uses the globals of ${globals.join(', ')}, list them in compilerOptions.types.`);
373
+ }
374
+ return { updated };
375
+ };
376
+ }
377
+ /**
378
+ * A test config extending tsconfig.json would no longer load Jest's globals.
379
+ */
380
+ testTypes() {
381
+ return (_file, code, advanced) => {
382
+ var _a;
383
+ if (((_a = advanced === null || advanced === void 0 ? void 0 : advanced.compilerOptions) === null || _a === void 0 ? void 0 : _a.types) !== undefined)
384
+ return null;
385
+ const pkg = this.packageJson();
386
+ const listed = Object.assign(Object.assign({}, pkg.dependencies), pkg.devDependencies);
387
+ if (!['@flarum/jest-config', 'jest', '@types/jest'].some((name) => listed[name]))
388
+ return null;
389
+ const updated = insertCompilerOption(code, null, '"types": ["jest"]');
390
+ return updated ? { updated } : null;
391
+ };
392
+ }
393
+ /**
394
+ * Whether TypeScript 6 and newer accept the extension's tsconfig.json: it
395
+ * replaces the moduleResolution flarum-tsconfig sets, which they refuse, and
396
+ * sets no baseUrl, which they refuse too.
397
+ */
398
+ tsConfigReadsWithSixOrNewer() {
399
+ var _a, _b;
400
+ try {
401
+ const code = fs_1.default.readFileSync(path_1.default.join(this.root, 'js/tsconfig.json'), 'utf8');
402
+ const options = (_b = (_a = this.advancedContent('js/tsconfig.json', code)) === null || _a === void 0 ? void 0 : _a.compilerOptions) !== null && _b !== void 0 ? _b : {};
403
+ return ['bundler', 'node16', 'nodenext'].includes(String(options.moduleResolution).toLowerCase()) && options.baseUrl === undefined;
404
+ }
405
+ catch (_c) {
406
+ return false;
407
+ }
408
+ }
409
+ /** The extension's js/package.json, or nothing when it has none. */
410
+ packageJson() {
411
+ try {
412
+ return JSON.parse(fs_1.default.readFileSync(path_1.default.join(this.root, 'js/package.json'), 'utf8'));
413
+ }
414
+ catch (_a) {
415
+ return {};
416
+ }
417
+ }
121
418
  }
122
419
  exports.default = Dependencies;
@@ -0,0 +1,86 @@
1
+ import { Binding, NodePath } from '@babel/traverse';
2
+ import * as t from '@babel/types';
3
+ import { BaseUpgradeStep, GitCommit, Replacement } from '../base';
4
+ /** A registration chain written directly in an admin initializer. */
5
+ type Registration = {
6
+ statement: NodePath<t.ExpressionStatement>;
7
+ calls: t.CallExpression[];
8
+ /** What for() names, when it is a string the upgrade can read. */
9
+ context: string | null;
10
+ /** Why it can't move, if it can't. */
11
+ reason: string | null;
12
+ imports: Map<string, Binding>;
13
+ constants: Set<Binding>;
14
+ /** Whether its arguments use app without importing it. */
15
+ usesGlobalApp: boolean;
16
+ };
17
+ /**
18
+ * Flarum 2.0 registers an extension's settings, permissions and page with the
19
+ * Admin extender, in admin/extend, which is declarative and runs when the admin
20
+ * mounts, rather than through the registry in an initializer.
21
+ *
22
+ * Registrations move only when they would do the same thing from there: each
23
+ * uses nothing the initializer declares, and nothing else registers with the
24
+ * registry as the admin boots, since registering later than that would change
25
+ * their order. Otherwise, all of the extension's registrations stay, with a
26
+ * @TODO saying why.
27
+ */
28
+ export default class AdminExtender extends BaseUpgradeStep {
29
+ type: string;
30
+ protected moved: string[];
31
+ protected left: string[];
32
+ replacements(file: string): Replacement[];
33
+ targets(): string[];
34
+ gitCommit(): GitCommit;
35
+ pauseMessage(): string;
36
+ protected upgrade(file: string, code: string): Promise<{
37
+ updated: string;
38
+ } | null>;
39
+ /**
40
+ * The registration chains written as statements of an admin initializer,
41
+ * `app.initializers.add(name, () => { ... })`.
42
+ */
43
+ protected registrations(program: NodePath<t.Program>): Registration[];
44
+ protected registration(statement: NodePath<t.ExpressionStatement>, initializer: NodePath<t.Function>, chain: Chain): Registration;
45
+ /**
46
+ * Why nothing can move because of other code using the registry: in another
47
+ * admin file, or in this one other than in a registration chain or in a
48
+ * function that runs later, through extend() or override().
49
+ */
50
+ protected otherRegistryUse(program: NodePath<t.Program>, registrations: Registration[], file: string): string | null;
51
+ /**
52
+ * Where the extender goes: the admin/extend the index exports, or a new one.
53
+ */
54
+ protected target(program: NodePath<t.Program>, file: string): Target | {
55
+ reason: string;
56
+ };
57
+ protected leave(file: string, code: string, registrations: Registration[], blocked: string | null): {
58
+ updated: string;
59
+ } | null;
60
+ protected move(file: string, code: string, program: NodePath<t.Program>, registrations: Registration[], target: Target): Promise<{
61
+ updated: string;
62
+ } | {
63
+ updated: string;
64
+ newFiles: {
65
+ path: string;
66
+ code: string;
67
+ }[];
68
+ } | null>;
69
+ /** The extension's ID, as core makes it from the package name. */
70
+ protected extensionId(): string | null;
71
+ }
72
+ type Chain = {
73
+ root: t.CallExpression;
74
+ calls: t.CallExpression[];
75
+ };
76
+ type Target = {
77
+ file: string;
78
+ /** The code of the extend file the index exports, if it already exists. */
79
+ existing: string | null;
80
+ /** The extenders outside admin the index exported, which the new file spreads. */
81
+ common: {
82
+ source: string;
83
+ declaration: t.ExportNamedDeclaration;
84
+ } | null;
85
+ };
86
+ export {};