@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.
- package/README.md +33 -1
- package/boilerplate/skeleton/extension/composer.json +4 -0
- package/boilerplate/skeleton/extension/js/package.json +5 -5
- package/boilerplate/skeleton/extension/js/tsconfig.json +3 -0
- package/boilerplate/skeleton/extension/js/tsconfig.test.json +1 -0
- package/boilerplate/stubs/backend/advanced-api-resource.php +3 -3
- package/boilerplate/stubs/backend/model-factory.php +17 -0
- package/boilerplate/stubs/backend/model.php +3 -0
- package/lib/base-command.d.ts +21 -3
- package/lib/base-command.js +52 -5
- package/lib/commands/make/backend/model.js +10 -0
- package/lib/commands/upgrade/2.0.d.ts +43 -0
- package/lib/commands/upgrade/2.0.js +128 -2
- package/lib/steps/gen-ext-scaffolder.d.ts +5 -0
- package/lib/steps/gen-ext-scaffolder.js +17 -2
- package/lib/steps/stubs/backend/advanced-api-resource.js +1 -1
- package/lib/steps/stubs/backend/model-factory.d.ts +24 -0
- package/lib/steps/stubs/backend/model-factory.js +33 -0
- package/lib/steps/upgrade/twopointoh/backend/email-views.js +22 -30
- package/lib/steps/upgrade/twopointoh/backend/extension-models.d.ts +30 -0
- package/lib/steps/upgrade/twopointoh/backend/extension-models.js +34 -0
- package/lib/steps/upgrade/twopointoh/backend/json-api.d.ts +6 -0
- package/lib/steps/upgrade/twopointoh/backend/json-api.js +14 -0
- package/lib/steps/upgrade/twopointoh/backend/misc.d.ts +6 -0
- package/lib/steps/upgrade/twopointoh/backend/misc.js +19 -7
- package/lib/steps/upgrade/twopointoh/backend/model-factories.d.ts +18 -0
- package/lib/steps/upgrade/twopointoh/backend/model-factories.js +73 -0
- package/lib/steps/upgrade/twopointoh/backend/phpunit.d.ts +2 -0
- package/lib/steps/upgrade/twopointoh/backend/phpunit.js +16 -8
- package/lib/steps/upgrade/twopointoh/backend/search.d.ts +9 -0
- package/lib/steps/upgrade/twopointoh/backend/search.js +28 -10
- package/lib/steps/upgrade/twopointoh/base.d.ts +38 -0
- package/lib/steps/upgrade/twopointoh/base.js +73 -12
- package/lib/steps/upgrade/twopointoh/dependencies.d.ts +55 -0
- package/lib/steps/upgrade/twopointoh/dependencies.js +317 -20
- package/lib/steps/upgrade/twopointoh/frontend/admin-extender.d.ts +86 -0
- package/lib/steps/upgrade/twopointoh/frontend/admin-extender.js +842 -0
- package/lib/steps/upgrade/twopointoh/frontend/code-splitting/usage-of-lazy-modules.js +110 -18
- package/lib/steps/upgrade/twopointoh/frontend/export-registry/import-ext.js +13 -16
- package/lib/steps/upgrade/twopointoh/frontend/misc.d.ts +23 -0
- package/lib/steps/upgrade/twopointoh/frontend/misc.js +147 -8
- package/lib/steps/upgrade/twopointoh/infra.d.ts +16 -1
- package/lib/steps/upgrade/twopointoh/infra.js +87 -5
- package/lib/steps/upgrade/twopointoh/step-failures.d.ts +8 -0
- package/lib/steps/upgrade/twopointoh/step-failures.js +6 -0
- package/lib/steps/upgrade/twopointoh/todos.d.ts +22 -0
- package/lib/steps/upgrade/twopointoh/todos.js +57 -0
- package/lib/steps/upgrade/twopointoh/typescript-5-9.d.ts +15 -0
- package/lib/steps/upgrade/twopointoh/typescript-5-9.js +250 -0
- package/lib/utils/ast.js +3 -2
- package/lib/utils/babel-ts.d.ts +9 -0
- package/lib/utils/babel-ts.js +18 -0
- package/lib/utils/composer.d.ts +5 -0
- package/lib/utils/composer.js +10 -1
- package/lib/utils/terminal.d.ts +13 -0
- package/lib/utils/terminal.js +26 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/php-subsystem/index.php +12 -0
- package/php-subsystem/src/NodeUtil.php +19 -9
- package/php-subsystem/src/NodeVisitors/AddExtender.php +49 -1
- package/php-subsystem/src/Upgrade/TwoPointOh/JsonApi.php +2 -20
- package/php-subsystem/src/Upgrade/TwoPointOh/Misc.php +13 -17
- package/php-subsystem/src/Upgrade/TwoPointOh/ModelFactories.php +943 -0
- package/php-subsystem/src/Upgrade/TwoPointOh/PhpUnit.php +476 -71
- package/php-subsystem/src/Upgrade/TwoPointOh/Search.php +97 -13
|
@@ -11,13 +11,14 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
11
11
|
super(...arguments);
|
|
12
12
|
this.type = 'Adapt to accessing/extending lazy loaded Flarum modules.';
|
|
13
13
|
// Array.from(flarum.reg.chunks.entries().flatMap(e => e[1].modules.map(m => e[1].namespace + ':' + m))).join('\n')
|
|
14
|
+
// LabelValue and LoadingPost were lazy once, and are in the main bundle
|
|
15
|
+
// now (flarum/framework#4235), so they are imported as usual.
|
|
14
16
|
// core:forum/components/ReplyComposer
|
|
15
17
|
// core:forum/components/ComposerBody
|
|
16
18
|
// core:common/components/ConfirmDocumentUnload
|
|
17
19
|
// core:forum/components/LogInModal
|
|
18
20
|
// core:forum/components/ForgotPasswordModal
|
|
19
21
|
// core:forum/components/PostStream
|
|
20
|
-
// core:forum/components/LoadingPost
|
|
21
22
|
// core:forum/components/ReplyPlaceholder
|
|
22
23
|
// core:forum/components/PostStreamScrubber
|
|
23
24
|
// core:forum/components/SearchModal
|
|
@@ -36,7 +37,6 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
36
37
|
// core:forum/components/AccessTokensList
|
|
37
38
|
// core:forum/components/NewAccessTokenModal
|
|
38
39
|
// core:forum/states/UserSecurityPageState
|
|
39
|
-
// core:common/components/LabelValue
|
|
40
40
|
// core:forum/components/NotificationsPage
|
|
41
41
|
// core:forum/components/Composer
|
|
42
42
|
// core:forum/components/ComposerButton
|
|
@@ -53,15 +53,11 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
53
53
|
'ForgotPasswordModal',
|
|
54
54
|
'NotificationsPage',
|
|
55
55
|
'ComposerBody',
|
|
56
|
-
'ConfirmDocumentUnload',
|
|
57
|
-
'LoadingPost',
|
|
58
56
|
'ReplyPlaceholder',
|
|
59
57
|
'PostStreamScrubber',
|
|
60
|
-
'SearchModal',
|
|
61
58
|
'SignUpModal',
|
|
62
59
|
'DiscussionComposer',
|
|
63
60
|
'EditPostComposer',
|
|
64
|
-
'ComposerBody',
|
|
65
61
|
'LogInModal',
|
|
66
62
|
'PostStream',
|
|
67
63
|
'ReplyComposer',
|
|
@@ -75,16 +71,16 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
75
71
|
'UserSecurityPageState',
|
|
76
72
|
'ComposerButton',
|
|
77
73
|
],
|
|
78
|
-
common: ['EditUserModal', '
|
|
74
|
+
common: ['EditUserModal', 'ConfirmDocumentUnload', 'SearchModal'],
|
|
79
75
|
},
|
|
80
76
|
'flarum/tags': {
|
|
81
77
|
admin: [],
|
|
82
|
-
forum: ['TagDiscussionModal', '
|
|
83
|
-
common: [],
|
|
78
|
+
forum: ['TagDiscussionModal', 'ToggleButton'],
|
|
79
|
+
common: ['TagSelectionModal'],
|
|
84
80
|
},
|
|
85
81
|
'flarum/emoji': {
|
|
86
82
|
admin: [],
|
|
87
|
-
forum: ['
|
|
83
|
+
forum: ['emojiMap'],
|
|
88
84
|
common: [],
|
|
89
85
|
},
|
|
90
86
|
current: {
|
|
@@ -184,11 +180,16 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
184
180
|
updateComposerLoad() {
|
|
185
181
|
return (_file, code, advanced) => {
|
|
186
182
|
const ast = advanced;
|
|
183
|
+
let addsOverride = false;
|
|
187
184
|
this.forEachLazyModule(code, ({ module, importPath }) => {
|
|
188
185
|
// before = app.composer.load(${module}, { ... }, ...);
|
|
189
186
|
// after = app.composer.load(() => import('${importPath}'), { ... }, ...);
|
|
190
187
|
const lines = [];
|
|
191
|
-
|
|
188
|
+
// A composer's prototype lines move into the then() of the file's own
|
|
189
|
+
// load of it, which has the module. Without one, they are given like
|
|
190
|
+
// any other lazy module's.
|
|
191
|
+
const movesToLoad = this.composers.includes(module) && loadsComposer(ast, module);
|
|
192
|
+
const overrideName = importedName(ast, 'flarum/common/extend', 'override');
|
|
192
193
|
// pick up {module}.prototype... lines to add to the .then(() => { lines }) at the end of the call expression.
|
|
193
194
|
(0, traverse_1.default)(ast, {
|
|
194
195
|
ExpressionStatement(path) {
|
|
@@ -212,17 +213,18 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
212
213
|
return;
|
|
213
214
|
if (object.name !== module)
|
|
214
215
|
return;
|
|
215
|
-
if (
|
|
216
|
+
if (movesToLoad) {
|
|
216
217
|
lines.push(node);
|
|
217
218
|
path.remove();
|
|
218
219
|
}
|
|
219
220
|
else if ((t.isFunctionExpression(expression.right) || t.isArrowFunctionExpression(expression.right)) &&
|
|
220
|
-
(t.isIdentifier(left.property) || t.isStringLiteral(left.property))) {
|
|
221
|
-
// SettingsPage.prototype['draftsItems'] =
|
|
222
|
-
// change to
|
|
221
|
+
((t.isIdentifier(left.property) && !left.computed) || t.isStringLiteral(left.property))) {
|
|
222
|
+
// SettingsPage.prototype['draftsItems'] = function () {...};
|
|
223
|
+
// change to override('flarum/forum/components/SettingsPage', 'draftsItems', function (original) {...});
|
|
223
224
|
const methodName = t.isIdentifier(left.property) ? left.property.name : left.property.value;
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
path.replaceWith(overrideMethod(overrideName !== null && overrideName !== void 0 ? overrideName : 'override', importPath, methodName, expression.right));
|
|
226
|
+
if (!overrideName)
|
|
227
|
+
addsOverride = true;
|
|
226
228
|
}
|
|
227
229
|
},
|
|
228
230
|
});
|
|
@@ -307,6 +309,7 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
307
309
|
});
|
|
308
310
|
return {
|
|
309
311
|
updated: ast,
|
|
312
|
+
imports: addsOverride ? [{ import: { name: 'override', path: 'flarum/common/extend', defaultImport: false } }] : [],
|
|
310
313
|
};
|
|
311
314
|
};
|
|
312
315
|
}
|
|
@@ -349,10 +352,28 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
349
352
|
}
|
|
350
353
|
removeImportLines() {
|
|
351
354
|
return (_file, code) => {
|
|
352
|
-
this.forEachLazyModule(code, ({ module, importLine }) => {
|
|
355
|
+
this.forEachLazyModule(code, ({ module, importPath, importedAs, importLine }) => {
|
|
356
|
+
var _a, _b;
|
|
353
357
|
if (code.includes(` extends ${module}`)) {
|
|
354
358
|
return;
|
|
355
359
|
}
|
|
360
|
+
// Types are erased from the build, so a type import costs nothing.
|
|
361
|
+
if (/^type\s/.test(importedAs.trim())) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// The other replacements load what they can as it's needed. Whatever
|
|
365
|
+
// still uses the import, such as JSX rendering the component, would
|
|
366
|
+
// break without it, and only the author knows where to load it. A name
|
|
367
|
+
// inside a string, such as a module path, is no use of it.
|
|
368
|
+
const names = (_b = (_a = importedAs.match(/[$A-Z_a-z][\w$]*/g)) === null || _a === void 0 ? void 0 : _a.filter((name) => name !== 'as' && name !== 'default')) !== null && _b !== void 0 ? _b : [];
|
|
369
|
+
const rest = code.replace(importLine, '');
|
|
370
|
+
if (names.some((name) => new RegExp(`(?<![\\w$./'"])${name.replace(/\$/g, '\\$')}(?![\\w$'"])`).test(rest))) {
|
|
371
|
+
const todo = `// @TODO: ${module} is lazy loaded in 2.0, so this import is undefined until its chunk has loaded. Load it with \`await import('${importPath}')\` before using it. https://docs.flarum.org/2.x/extend/code-splitting\n`;
|
|
372
|
+
if (!code.includes(todo + importLine)) {
|
|
373
|
+
code = code.replace(importLine, todo + importLine);
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
356
377
|
code = code
|
|
357
378
|
// then remove the import.
|
|
358
379
|
.replace(importLine, '');
|
|
@@ -416,3 +437,74 @@ class UsageOfLazyModules extends base_1.BaseUpgradeStep {
|
|
|
416
437
|
}
|
|
417
438
|
}
|
|
418
439
|
exports.default = UsageOfLazyModules;
|
|
440
|
+
/** Whether the file loads the composer itself, with app.composer.load(Composer, ...). */
|
|
441
|
+
function loadsComposer(ast, module) {
|
|
442
|
+
let loads = false;
|
|
443
|
+
(0, traverse_1.default)(ast, {
|
|
444
|
+
CallExpression(path) {
|
|
445
|
+
const callee = path.node.callee;
|
|
446
|
+
const firstArg = path.node.arguments[0];
|
|
447
|
+
if (t.isMemberExpression(callee) &&
|
|
448
|
+
t.isIdentifier(callee.property, { name: 'load' }) &&
|
|
449
|
+
t.isMemberExpression(callee.object) &&
|
|
450
|
+
t.isIdentifier(callee.object.property, { name: 'composer' }) &&
|
|
451
|
+
t.isIdentifier(callee.object.object, { name: 'app' }) &&
|
|
452
|
+
t.isIdentifier(firstArg, { name: module })) {
|
|
453
|
+
loads = true;
|
|
454
|
+
path.stop();
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
return loads;
|
|
459
|
+
}
|
|
460
|
+
/** The name a file imports one of a module's exports by, if it does. */
|
|
461
|
+
function importedName(ast, source, name) {
|
|
462
|
+
for (const node of ast.program.body) {
|
|
463
|
+
if (!t.isImportDeclaration(node) || node.source.value !== source)
|
|
464
|
+
continue;
|
|
465
|
+
for (const specifier of node.specifiers) {
|
|
466
|
+
if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported, { name })) {
|
|
467
|
+
return specifier.local.name;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
/** A name for a new binding in the function, that hides none it uses. */
|
|
474
|
+
function unusedName(node, name) {
|
|
475
|
+
const used = new Set();
|
|
476
|
+
t.traverseFast(node, (child) => {
|
|
477
|
+
if (t.isIdentifier(child))
|
|
478
|
+
used.add(child.name);
|
|
479
|
+
});
|
|
480
|
+
while (used.has(name))
|
|
481
|
+
name = '_' + name;
|
|
482
|
+
return name;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Module.prototype.method = fn; given with override() by the module's path,
|
|
486
|
+
* which replaces the method as the assignment did once the module loads. The
|
|
487
|
+
* original method comes first, after a TypeScript this parameter: a function
|
|
488
|
+
* that reads its arguments with `arguments` is called with them as they were.
|
|
489
|
+
*/
|
|
490
|
+
function overrideMethod(override, importPath, methodName, method) {
|
|
491
|
+
const original = t.identifier(unusedName(method, 'original'));
|
|
492
|
+
let replacement;
|
|
493
|
+
let readsArguments = false;
|
|
494
|
+
t.traverseFast(method, (child) => {
|
|
495
|
+
if (t.isIdentifier(child, { name: 'arguments' }))
|
|
496
|
+
readsArguments = true;
|
|
497
|
+
});
|
|
498
|
+
if (t.isFunctionExpression(method) && readsArguments) {
|
|
499
|
+
const args = t.identifier(unusedName(method, 'args'));
|
|
500
|
+
const call = t.callExpression(t.memberExpression(method, t.identifier('apply')), [t.thisExpression(), args]);
|
|
501
|
+
replacement = t.functionExpression(null, [original, t.restElement(args)], t.blockStatement([t.returnStatement(call)]));
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
const first = method.params[0];
|
|
505
|
+
const at = t.isIdentifier(first, { name: 'this' }) ? 1 : 0;
|
|
506
|
+
replacement = t.cloneNode(method);
|
|
507
|
+
replacement.params.splice(at, 0, original);
|
|
508
|
+
}
|
|
509
|
+
return t.expressionStatement(t.callExpression(t.identifier(override), [t.stringLiteral(importPath), t.stringLiteral(methodName), replacement]));
|
|
510
|
+
}
|
|
@@ -77,17 +77,12 @@ class ImportExt extends base_1.BaseUpgradeStep {
|
|
|
77
77
|
updateImportsFromFlarumExtensions() {
|
|
78
78
|
return (_file, code) => {
|
|
79
79
|
// replace `import module from 'flarum/ext/module-path';` with `import module from 'ext:flarum/extension/module-path';`
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.importingFromExtensions
|
|
86
|
-
...this.importingFromExtensions,
|
|
87
|
-
...imports.map((imp) => {
|
|
88
|
-
return imp.match(/from ["'](flarum\/\w+).*\/([\w/]+)["']/)[1];
|
|
89
|
-
}),
|
|
90
|
-
];
|
|
80
|
+
// wherever the code names a module: in imports and exports of any kind, import(), require(),
|
|
81
|
+
// and `declare module`, which augments the module. extension must not be one of [forum, admin, common].
|
|
82
|
+
const regex = /(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bdeclare\s+module\s+|\bimport\s+)(["'])(flarum\/(?!\b(?:common|admin|forum|utils|helpers|components|models)\b)[\w-]+\/[^\s"']+)\2/gm;
|
|
83
|
+
// First collect all the extensions imported from
|
|
84
|
+
for (const match of code.matchAll(regex)) {
|
|
85
|
+
this.importingFromExtensions.push(match[3].split('/').slice(0, 2).join('/'));
|
|
91
86
|
}
|
|
92
87
|
// add todos to update imports that use other extensions
|
|
93
88
|
const uniqueUseExtensions = [...new Set(this.useExtensions)];
|
|
@@ -96,7 +91,7 @@ class ImportExt extends base_1.BaseUpgradeStep {
|
|
|
96
91
|
});
|
|
97
92
|
return {
|
|
98
93
|
updated: code
|
|
99
|
-
.replace(regex,
|
|
94
|
+
.replace(regex, '$1$2ext:$3$2')
|
|
100
95
|
// imports from @flarum/core no longer supported, replace with flarum/
|
|
101
96
|
.replace(/import\s+(\w+)\s+from\s+["']@flarum\/core\/([\w/]+)["']/gim, `import $1 from 'flarum/$2'`),
|
|
102
97
|
};
|
|
@@ -112,10 +107,12 @@ class ImportExt extends base_1.BaseUpgradeStep {
|
|
|
112
107
|
(_a = advanced.compilerOptions).paths || (_a.paths = {});
|
|
113
108
|
const paths = advanced.compilerOptions.paths;
|
|
114
109
|
uniqueExtensions.forEach((ext) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
paths[
|
|
110
|
+
var _a;
|
|
111
|
+
// Where the 1.x mapping found the typings, which isn't always vendor.
|
|
112
|
+
const typings = paths[`${ext}/*`];
|
|
113
|
+
delete paths[ext];
|
|
114
|
+
delete paths[`${ext}/*`];
|
|
115
|
+
paths[_a = `ext:${ext}/*`] || (paths[_a] = typings !== null && typings !== void 0 ? typings : [`../vendor/${ext}/js/dist-typings/*`]);
|
|
119
116
|
});
|
|
120
117
|
return {
|
|
121
118
|
updated: advanced,
|
|
@@ -2,6 +2,13 @@ import { BaseUpgradeStep, GitCommit, Replacement } from '../base';
|
|
|
2
2
|
export default class MiscFrontendChanges extends BaseUpgradeStep {
|
|
3
3
|
type: string;
|
|
4
4
|
replacements(file: string): Replacement[];
|
|
5
|
+
/**
|
|
6
|
+
* 2.0's AvatarEditor opens the file picker on click, and uploads a dropped
|
|
7
|
+
* image, whenever user.hasUploadedAvatar() is false. 1.x went by
|
|
8
|
+
* avatarUrl(), so an extension that gives users an avatar some other way,
|
|
9
|
+
* as a Gravatar one does, now gets an upload instead of its menu.
|
|
10
|
+
*/
|
|
11
|
+
flagAvatarEditor(): Replacement;
|
|
5
12
|
targets(): string[];
|
|
6
13
|
gitCommit(): GitCommit;
|
|
7
14
|
pauseMessage(): string;
|
|
@@ -11,7 +18,23 @@ export default class MiscFrontendChanges extends BaseUpgradeStep {
|
|
|
11
18
|
private updateIndexPage;
|
|
12
19
|
private updateAvatarIcon;
|
|
13
20
|
private updateOtherRefs;
|
|
21
|
+
/**
|
|
22
|
+
* `app.initializers.has(name)` becomes `'id' in flarum.extensions`, where the
|
|
23
|
+
* upgrade knows the ID for certain: a bundled extension's, or one the
|
|
24
|
+
* extension's composer.json names. An initializer is not always named after
|
|
25
|
+
* its extension, so a check it can't be sure of gets a @TODO instead.
|
|
26
|
+
*/
|
|
14
27
|
private updateInitializers;
|
|
28
|
+
/**
|
|
29
|
+
* For `app.initializers.has('name')`: the ID of the extension it checks for,
|
|
30
|
+
* or null when the upgrade can't be sure of it. Undefined for anything else.
|
|
31
|
+
*/
|
|
32
|
+
private checkedExtension;
|
|
33
|
+
private named;
|
|
34
|
+
/**
|
|
35
|
+
* The extensions the composer.json names, by package name and by ID.
|
|
36
|
+
*/
|
|
37
|
+
private namedExtensions;
|
|
15
38
|
private updateUploadImageButton;
|
|
16
39
|
private updateExtendModal;
|
|
17
40
|
}
|
|
@@ -5,11 +5,50 @@ const base_1 = require("../base");
|
|
|
5
5
|
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
6
6
|
const t = tslib_1.__importStar(require("@babel/types"));
|
|
7
7
|
const traverse_1 = tslib_1.__importDefault(require("@babel/traverse"));
|
|
8
|
+
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
9
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
8
10
|
const ast_1 = require("../../../../utils/ast");
|
|
11
|
+
const babel_ts_1 = require("../../../../utils/babel-ts");
|
|
12
|
+
const composer_1 = require("../../../../utils/composer");
|
|
13
|
+
/** The bundled extensions' IDs, which are their initializers' names in 2.0. */
|
|
14
|
+
const BUNDLED = new Set([
|
|
15
|
+
'flarum-akismet',
|
|
16
|
+
'flarum-approval',
|
|
17
|
+
'flarum-audit',
|
|
18
|
+
'flarum-bbcode',
|
|
19
|
+
'flarum-deck',
|
|
20
|
+
'flarum-embed',
|
|
21
|
+
'flarum-emoji',
|
|
22
|
+
'flarum-extension-manager',
|
|
23
|
+
'flarum-flags',
|
|
24
|
+
'flarum-gdpr',
|
|
25
|
+
'flarum-lang-english',
|
|
26
|
+
'flarum-likes',
|
|
27
|
+
'flarum-lock',
|
|
28
|
+
'flarum-markdown',
|
|
29
|
+
'flarum-mentions',
|
|
30
|
+
'flarum-messages',
|
|
31
|
+
'flarum-nicknames',
|
|
32
|
+
'flarum-pusher',
|
|
33
|
+
'flarum-realtime',
|
|
34
|
+
'flarum-statistics',
|
|
35
|
+
'flarum-sticky',
|
|
36
|
+
'flarum-subscriptions',
|
|
37
|
+
'flarum-suspend',
|
|
38
|
+
'flarum-tags',
|
|
39
|
+
]);
|
|
40
|
+
/** The 1.x names of the bundled initializers 2.0 renamed (flarum/framework#4003). */
|
|
41
|
+
const RENAMED = {
|
|
42
|
+
lock: 'flarum-lock',
|
|
43
|
+
subscriptions: 'flarum-subscriptions',
|
|
44
|
+
'flarum/nicknames': 'flarum-nicknames',
|
|
45
|
+
};
|
|
46
|
+
const CHECK_TODO = "@TODO: check whether the extension is enabled with '<its extension ID>' in flarum.extensions instead";
|
|
9
47
|
class MiscFrontendChanges extends base_1.BaseUpgradeStep {
|
|
10
48
|
constructor() {
|
|
11
49
|
super(...arguments);
|
|
12
50
|
this.type = 'Miscellaneous frontend changes.';
|
|
51
|
+
this.named = null;
|
|
13
52
|
}
|
|
14
53
|
// - [`IndexPage.prototype.sidebar` -> `IndexSidebar`](https://github.com/flarum/framework/blob/72f89c0209a5c4bbdc5482ecbdc2435dcd57550f/framework/core/js/src/forum/components/IndexSidebar.tsx)
|
|
15
54
|
// - [`IndexPage.prototype.navItems` -> `IndexSidebar.prototype.navItems`](https://github.com/flarum/framework/blob/72f89c0209a5c4bbdc5482ecbdc2435dcd57550f/framework/core/js/src/forum/components/IndexSidebar.tsx)
|
|
@@ -31,8 +70,23 @@ class MiscFrontendChanges extends base_1.BaseUpgradeStep {
|
|
|
31
70
|
this.updateInitializers(),
|
|
32
71
|
this.updateUploadImageButton(),
|
|
33
72
|
...this.updateExtendModal(),
|
|
73
|
+
this.flagAvatarEditor(),
|
|
34
74
|
];
|
|
35
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* 2.0's AvatarEditor opens the file picker on click, and uploads a dropped
|
|
78
|
+
* image, whenever user.hasUploadedAvatar() is false. 1.x went by
|
|
79
|
+
* avatarUrl(), so an extension that gives users an avatar some other way,
|
|
80
|
+
* as a Gravatar one does, now gets an upload instead of its menu.
|
|
81
|
+
*/
|
|
82
|
+
flagAvatarEditor() {
|
|
83
|
+
return (file, code) => {
|
|
84
|
+
if (/["']flarum\/forum\/components\/AvatarEditor["']/.test(code)) {
|
|
85
|
+
this.leaveTodo(file, 'It uses AvatarEditor, which in 2.0 opens the file picker, or uploads a dropped image, whenever user.hasUploadedAvatar() is false, not when there is no avatarUrl(). Check what it does for users without an uploaded avatar.');
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
36
90
|
targets() {
|
|
37
91
|
return ['js/src/**/*'];
|
|
38
92
|
}
|
|
@@ -258,8 +312,7 @@ class MiscFrontendChanges extends base_1.BaseUpgradeStep {
|
|
|
258
312
|
return t.jsxAttribute(t.jsxIdentifier(key.toString()), t.isStringLiteral(prop.value) ? prop.value : t.jsxExpressionContainer(prop.value));
|
|
259
313
|
}
|
|
260
314
|
catch (error) {
|
|
261
|
-
|
|
262
|
-
throw error;
|
|
315
|
+
throw new Error(`Could not turn the "${key.toString()}" attr into a JSX attribute: ${error.message}`);
|
|
263
316
|
}
|
|
264
317
|
});
|
|
265
318
|
}
|
|
@@ -315,7 +368,8 @@ class MiscFrontendChanges extends base_1.BaseUpgradeStep {
|
|
|
315
368
|
updated: code
|
|
316
369
|
.replace(/this\.currentTag/g, 'app.currentTag')
|
|
317
370
|
.replace('app.search.', 'app.search.state.')
|
|
318
|
-
.replace(
|
|
371
|
+
.replace(/\bapp\.extensionData\b/g, 'app.registry')
|
|
372
|
+
.replace(/\b(app\.registry\s*\.\s*)getAllExtensionPermissions\b/g, '$1getAllPermissions')
|
|
319
373
|
.replace("extend(BasicsPage.prototype, 'homePageItems'", "extend(BasicsPage, 'homePageItems'")
|
|
320
374
|
.replace('className="NotificationList', 'className="HeaderList')
|
|
321
375
|
.replace('className: "NotificationList', 'className: "HeaderList')
|
|
@@ -325,18 +379,103 @@ class MiscFrontendChanges extends base_1.BaseUpgradeStep {
|
|
|
325
379
|
};
|
|
326
380
|
};
|
|
327
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* `app.initializers.has(name)` becomes `'id' in flarum.extensions`, where the
|
|
384
|
+
* upgrade knows the ID for certain: a bundled extension's, or one the
|
|
385
|
+
* extension's composer.json names. An initializer is not always named after
|
|
386
|
+
* its extension, so a check it can't be sure of gets a @TODO instead.
|
|
387
|
+
*/
|
|
328
388
|
updateInitializers() {
|
|
329
389
|
return (_file, code) => {
|
|
330
|
-
if (
|
|
390
|
+
if (!/initializers\s*\.\s*has\s*\(/.test(code))
|
|
391
|
+
return null;
|
|
392
|
+
const edits = [];
|
|
393
|
+
const flagged = new Set();
|
|
394
|
+
(0, traverse_1.default)((0, babel_ts_1.parse)(code), {
|
|
395
|
+
CallExpression: (p) => {
|
|
396
|
+
var _a;
|
|
397
|
+
const id = this.checkedExtension(p.node);
|
|
398
|
+
if (id === undefined)
|
|
399
|
+
return;
|
|
400
|
+
if (id) {
|
|
401
|
+
edits.push({ start: p.node.start, end: p.node.end, text: `('${id}' in flarum.extensions)` });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Above the statement, where a comment can't land in JSX or a string.
|
|
405
|
+
const statement = p.getStatementParent();
|
|
406
|
+
if (!statement || ((_a = statement.node.leadingComments) === null || _a === void 0 ? void 0 : _a.some((comment) => comment.value.includes(CHECK_TODO))))
|
|
407
|
+
return;
|
|
408
|
+
const lineStart = code.lastIndexOf('\n', statement.node.start - 1) + 1;
|
|
409
|
+
if (flagged.has(lineStart))
|
|
410
|
+
return;
|
|
411
|
+
flagged.add(lineStart);
|
|
412
|
+
const indent = /^[\t ]*/.exec(code.slice(lineStart))[0];
|
|
413
|
+
const docs = 'https://docs.flarum.org/2.x/extend/extending-extensions';
|
|
414
|
+
edits.push({
|
|
415
|
+
start: lineStart,
|
|
416
|
+
end: lineStart,
|
|
417
|
+
text: `${indent}// ${CHECK_TODO} (${docs}). An initializer is not always named after its extension, and some were renamed in 2.0.\n`,
|
|
418
|
+
});
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
if (edits.length === 0)
|
|
331
422
|
return null;
|
|
332
423
|
return {
|
|
333
|
-
updated:
|
|
334
|
-
.
|
|
335
|
-
.
|
|
336
|
-
.replace(/initializers\.has\(["']flarum\/nicknames["']\)/g, "initializers.add('flarum-nicknames')"),
|
|
424
|
+
updated: edits
|
|
425
|
+
.sort((a, b) => b.start - a.start)
|
|
426
|
+
.reduce((result, edit) => result.slice(0, edit.start) + edit.text + result.slice(edit.end), code),
|
|
337
427
|
};
|
|
338
428
|
};
|
|
339
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* For `app.initializers.has('name')`: the ID of the extension it checks for,
|
|
432
|
+
* or null when the upgrade can't be sure of it. Undefined for anything else.
|
|
433
|
+
*/
|
|
434
|
+
checkedExtension(call) {
|
|
435
|
+
var _a, _b;
|
|
436
|
+
const callee = call.callee;
|
|
437
|
+
if (!t.isMemberExpression(callee) || callee.computed || !t.isIdentifier(callee.property, { name: 'has' }))
|
|
438
|
+
return undefined;
|
|
439
|
+
const initializers = callee.object;
|
|
440
|
+
if (!t.isMemberExpression(initializers) || initializers.computed || !t.isIdentifier(initializers.property, { name: 'initializers' })) {
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
const owner = initializers.object;
|
|
444
|
+
if (!t.isIdentifier(owner, { name: 'app' }) && !(t.isMemberExpression(owner) && t.isIdentifier(owner.property, { name: 'app' }))) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const [name] = call.arguments;
|
|
448
|
+
if (call.arguments.length !== 1 || !t.isStringLiteral(name))
|
|
449
|
+
return undefined;
|
|
450
|
+
return (_a = RENAMED[name.value]) !== null && _a !== void 0 ? _a : (BUNDLED.has(name.value) ? name.value : (_b = this.namedExtensions().get(name.value)) !== null && _b !== void 0 ? _b : null);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The extensions the composer.json names, by package name and by ID.
|
|
454
|
+
*/
|
|
455
|
+
namedExtensions() {
|
|
456
|
+
var _a, _b, _c, _d, _e, _f;
|
|
457
|
+
if (this.named)
|
|
458
|
+
return this.named;
|
|
459
|
+
this.named = new Map();
|
|
460
|
+
let composer = {};
|
|
461
|
+
try {
|
|
462
|
+
composer = JSON.parse(fs_1.default.readFileSync(path_1.default.join(this.root, 'composer.json'), 'utf8'));
|
|
463
|
+
}
|
|
464
|
+
catch (_g) {
|
|
465
|
+
return this.named;
|
|
466
|
+
}
|
|
467
|
+
const packages = [
|
|
468
|
+
...Object.keys((_a = composer.require) !== null && _a !== void 0 ? _a : {}),
|
|
469
|
+
...Object.keys((_b = composer['require-dev']) !== null && _b !== void 0 ? _b : {}),
|
|
470
|
+
...Object.keys((_c = composer.suggest) !== null && _c !== void 0 ? _c : {}),
|
|
471
|
+
...((_f = (_e = (_d = composer.extra) === null || _d === void 0 ? void 0 : _d['flarum-extension']) === null || _e === void 0 ? void 0 : _e['optional-dependencies']) !== null && _f !== void 0 ? _f : []),
|
|
472
|
+
].filter((name) => typeof name === 'string' && name.includes('/'));
|
|
473
|
+
for (const name of packages) {
|
|
474
|
+
this.named.set(name, (0, composer_1.extensionId)(name));
|
|
475
|
+
this.named.set((0, composer_1.extensionId)(name), (0, composer_1.extensionId)(name));
|
|
476
|
+
}
|
|
477
|
+
return this.named;
|
|
478
|
+
}
|
|
340
479
|
updateUploadImageButton() {
|
|
341
480
|
return (file, code) => {
|
|
342
481
|
if (!code.includes('UploadImageButton'))
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { BaseUpgradeStep, GitCommit, Replacement } from './base';
|
|
1
|
+
import { AdvancedContent, BaseUpgradeStep, GitCommit, Replacement } from './base';
|
|
2
2
|
export default class Infrastructure extends BaseUpgradeStep {
|
|
3
3
|
type: string;
|
|
4
4
|
replacements(file: string): Replacement[];
|
|
5
5
|
targets(): string[];
|
|
6
|
+
advancedContent(_file: string, _code: string): AdvancedContent;
|
|
6
7
|
gitCommit(): GitCommit;
|
|
7
8
|
pauseMessage(): string;
|
|
8
9
|
updateWorkflow(): Replacement;
|
|
10
|
+
/**
|
|
11
|
+
* The reusable frontend workflow commits the built JS to `main_git_branch`,
|
|
12
|
+
* which still names 1.x. Point it at a 2.x branch when there is one. Other
|
|
13
|
+
* values are left alone: an extension's 2.x code is often on main.
|
|
14
|
+
*/
|
|
15
|
+
updateMainGitBranch(code: string): Promise<string>;
|
|
16
|
+
/** Whether the extension's repository has the branch, locally or on a remote. */
|
|
17
|
+
protected hasBranch(name: string): Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Read the global typings of every Flarum package in vendor, as a new
|
|
20
|
+
* extension does, not only core's. The file is edited as text so its
|
|
21
|
+
* comments stay.
|
|
22
|
+
*/
|
|
23
|
+
updateTsConfigInclude(): Replacement;
|
|
9
24
|
updatePhpStan(): Replacement;
|
|
10
25
|
}
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const simple_git_1 = tslib_1.__importDefault(require("simple-git"));
|
|
3
5
|
const base_1 = require("./base");
|
|
6
|
+
// Where 1.x extensions got core's global typings from, and where 2.x ones get
|
|
7
|
+
// the global typings of every Flarum package they depend on.
|
|
8
|
+
const CORE_GLOBAL_TYPINGS = '"../vendor/flarum/core/js/dist-typings/@types/**/*"';
|
|
9
|
+
const VENDOR_GLOBAL_TYPINGS = '"../vendor/*/*/js/dist-typings/@types/**/*"';
|
|
10
|
+
const indentation = (line) => line.length - line.trimStart().length;
|
|
11
|
+
/**
|
|
12
|
+
* Drop a workflow's `php_versions` input, so the reusable workflow tests the
|
|
13
|
+
* PHP versions Flarum 2.0 supports and keeps up as that changes. A `with:`
|
|
14
|
+
* left without inputs goes too.
|
|
15
|
+
*/
|
|
16
|
+
function dropPhpVersions(code) {
|
|
17
|
+
const lines = code.split('\n');
|
|
18
|
+
const kept = [];
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
if (!/^\s*php_versions:/.test(lines[i])) {
|
|
21
|
+
kept.push(lines[i]);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// A folded or literal value continues on the more indented lines below.
|
|
25
|
+
while (i + 1 < lines.length && lines[i + 1].trim() !== '' && indentation(lines[i + 1]) > indentation(lines[i])) {
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return kept
|
|
30
|
+
.filter((line, i) => {
|
|
31
|
+
if (!/^\s*with:\s*(#.*)?$/.test(line))
|
|
32
|
+
return true;
|
|
33
|
+
const next = kept.slice(i + 1).find((other) => other.trim() !== '');
|
|
34
|
+
return next !== undefined && indentation(next) > indentation(line);
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
4
38
|
class Infrastructure extends base_1.BaseUpgradeStep {
|
|
5
39
|
constructor() {
|
|
6
40
|
super(...arguments);
|
|
@@ -14,10 +48,18 @@ class Infrastructure extends base_1.BaseUpgradeStep {
|
|
|
14
48
|
if (file.endsWith('.neon')) {
|
|
15
49
|
replacements.push(this.updatePhpStan());
|
|
16
50
|
}
|
|
51
|
+
if (file === 'js/tsconfig.json') {
|
|
52
|
+
replacements.push(this.updateTsConfigInclude());
|
|
53
|
+
}
|
|
17
54
|
return replacements;
|
|
18
55
|
}
|
|
19
56
|
targets() {
|
|
20
|
-
return ['.github/workflows/*.yml', 'phpstan.neon'];
|
|
57
|
+
return ['.github/workflows/*.yml', 'phpstan.neon', 'js/tsconfig.json'];
|
|
58
|
+
}
|
|
59
|
+
// Every file is only edited as text: a tsconfig.json the JSON parser can't
|
|
60
|
+
// read shouldn't stop the workflows being updated.
|
|
61
|
+
advancedContent(_file, _code) {
|
|
62
|
+
return null;
|
|
21
63
|
}
|
|
22
64
|
gitCommit() {
|
|
23
65
|
return {
|
|
@@ -29,15 +71,55 @@ class Infrastructure extends base_1.BaseUpgradeStep {
|
|
|
29
71
|
return 'Infrastructure updated. Please review the changes then proceed.';
|
|
30
72
|
}
|
|
31
73
|
updateWorkflow() {
|
|
32
|
-
return (_file, code) => ({
|
|
33
|
-
updated: code
|
|
74
|
+
return async (_file, code) => ({
|
|
75
|
+
updated: await this.updateMainGitBranch(dropPhpVersions(code
|
|
34
76
|
.replace('REUSABLE_backend.yml@1.x', 'REUSABLE_backend.yml@2.x')
|
|
35
77
|
.replace('REUSABLE_frontend.yml@1.x', 'REUSABLE_frontend.yml@2.x')
|
|
36
78
|
.replace('REUSABLE_backend.yml@main', 'REUSABLE_backend.yml@2.x')
|
|
37
|
-
.replace('REUSABLE_frontend.yml@main', 'REUSABLE_frontend.yml@2.x')
|
|
38
|
-
.replace(/php_versions: '\["[^']+]'/, 'php_versions: \'["8.2", "8.3", "8.4"]\''),
|
|
79
|
+
.replace('REUSABLE_frontend.yml@main', 'REUSABLE_frontend.yml@2.x'))),
|
|
39
80
|
});
|
|
40
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The reusable frontend workflow commits the built JS to `main_git_branch`,
|
|
84
|
+
* which still names 1.x. Point it at a 2.x branch when there is one. Other
|
|
85
|
+
* values are left alone: an extension's 2.x code is often on main.
|
|
86
|
+
*/
|
|
87
|
+
async updateMainGitBranch(code) {
|
|
88
|
+
const setting = /^([\t ]*)main_git_branch:([\t ]*)(["']?)1\.x\3/m;
|
|
89
|
+
const match = code.match(setting);
|
|
90
|
+
if (!match)
|
|
91
|
+
return code;
|
|
92
|
+
if (await this.hasBranch('2.x')) {
|
|
93
|
+
return code.replace(setting, (_line, indent, space, quote) => `${indent}main_git_branch:${space}${quote}2.x${quote}`);
|
|
94
|
+
}
|
|
95
|
+
const todo = `${match[1]}# @TODO: Set main_git_branch to the branch your Flarum 2.x code is on. The built JS is committed to it.`;
|
|
96
|
+
// An earlier run already left it.
|
|
97
|
+
if (code.includes(todo))
|
|
98
|
+
return code;
|
|
99
|
+
return code.replace(setting, (line) => `${todo}\n${line}`);
|
|
100
|
+
}
|
|
101
|
+
/** Whether the extension's repository has the branch, locally or on a remote. */
|
|
102
|
+
async hasBranch(name) {
|
|
103
|
+
try {
|
|
104
|
+
const branches = await (0, simple_git_1.default)(this.root).branch(['--all']);
|
|
105
|
+
return branches.all.some((branch) => branch === name || (branch.startsWith('remotes/') && branch.endsWith(`/${name}`)));
|
|
106
|
+
}
|
|
107
|
+
catch (_a) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Read the global typings of every Flarum package in vendor, as a new
|
|
113
|
+
* extension does, not only core's. The file is edited as text so its
|
|
114
|
+
* comments stay.
|
|
115
|
+
*/
|
|
116
|
+
updateTsConfigInclude() {
|
|
117
|
+
return (_file, code) => {
|
|
118
|
+
if (!code.includes(CORE_GLOBAL_TYPINGS) || code.includes(VENDOR_GLOBAL_TYPINGS))
|
|
119
|
+
return null;
|
|
120
|
+
return { updated: code.replace(CORE_GLOBAL_TYPINGS, VENDOR_GLOBAL_TYPINGS) };
|
|
121
|
+
};
|
|
122
|
+
}
|
|
41
123
|
updatePhpStan() {
|
|
42
124
|
return (_file, code) => ({
|
|
43
125
|
updated: code
|