From 2217d0c050d16aa195a65780ffd6c450ba202dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Sun, 10 Dec 2023 17:53:38 +0900 Subject: [PATCH] refactor(frontend): remove redundant class names (#12618) --- ...lugin-unwind-css-module-class-name.test.ts | 3 +- ...lup-plugin-unwind-css-module-class-name.ts | 223 +++++++++++++++++- .../frontend/src/components/MkCodeEditor.vue | 2 +- .../src/components/MkNoteDetailed.vue | 2 +- packages/frontend/src/components/MkSwitch.vue | 5 +- packages/frontend/src/pages/admin-user.vue | 7 +- .../src/pages/admin/modlog.ModLog.vue | 5 +- packages/frontend/src/ui/deck.vue | 2 +- packages/frontend/src/ui/universal.vue | 2 +- 9 files changed, 222 insertions(+), 29 deletions(-) diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts index 759f270393..550e08d7f7 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -180,7 +180,7 @@ import './photoswipe-!~{003}~.js'; const _hoisted_1 = createBaseVNode("i", { class: "ti ti-photo" }, null, -1); -const _sfc_main = defineComponent({ +const index_photos = defineComponent({ __name: "index.photos", props: { user: {} @@ -261,7 +261,6 @@ const style0 = { const cssModules = { "$style": style0 }; -const index_photos = _sfc_main; export {index_photos as default}; `.slice(1)); }); diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts index 18c817e0f5..68cdc0bc78 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts @@ -13,13 +13,13 @@ function isFalsyIdentifier(identifier: estree.Identifier): boolean { return identifier.name === 'undefined' || identifier.name === 'NaN'; } -function normalizeClassWalker(tree: estree.Node): string | null { +function normalizeClassWalker(tree: estree.Node, stack: string | undefined): string | null { if (tree.type === 'Identifier') return isFalsyIdentifier(tree) ? '' : null; if (tree.type === 'Literal') return typeof tree.value === 'string' ? tree.value : ''; if (tree.type === 'BinaryExpression') { if (tree.operator !== '+') return null; - const left = normalizeClassWalker(tree.left); - const right = normalizeClassWalker(tree.right); + const left = normalizeClassWalker(tree.left, stack); + const right = normalizeClassWalker(tree.right, stack); if (left === null || right === null) return null; return `${left}${right}`; } @@ -33,15 +33,15 @@ function normalizeClassWalker(tree: estree.Node): string | null { if (tree.type === 'ArrayExpression') { const values = tree.elements.map((treeNode) => { if (treeNode === null) return ''; - if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); - return normalizeClassWalker(treeNode); + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack); + return normalizeClassWalker(treeNode, stack); }); if (values.some((x) => x === null)) return null; return values.join(' '); } if (tree.type === 'ObjectExpression') { const values = tree.properties.map((treeNode) => { - if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument); + if (treeNode.type === 'SpreadElement') return normalizeClassWalker(treeNode.argument, stack); let x = treeNode.value; let inveted = false; while (x.type === 'UnaryExpression' && x.operator === '!') { @@ -67,18 +67,26 @@ function normalizeClassWalker(tree: estree.Node): string | null { if (values.some((x) => x === null)) return null; return values.join(' '); } - console.error(`Unexpected node type: ${tree.type}`); + if ( + tree.type !== 'CallExpression' && + tree.type !== 'ChainExpression' && + tree.type !== 'ConditionalExpression' && + tree.type !== 'LogicalExpression' && + tree.type !== 'MemberExpression') { + console.error(stack ? `Unexpected node type: ${tree.type} (in ${stack})` : `Unexpected node type: ${tree.type}`); + } return null; } -export function normalizeClass(tree: estree.Node): string | null { - const walked = normalizeClassWalker(tree); +export function normalizeClass(tree: estree.Node, stack?: string): string | null { + const walked = normalizeClassWalker(tree, stack); return walked && walked.replace(/^\s+|\s+(?=\s)|\s+$/g, ''); } export function unwindCssModuleClassName(ast: estree.Node): void { (walk as typeof estreeWalker.walk)(ast, { enter(node, parent): void { + //#region if (parent?.type !== 'Program') return; if (node.type !== 'VariableDeclaration') return; if (node.declarations.length !== 1) return; @@ -102,6 +110,14 @@ export function unwindCssModuleClassName(ast: estree.Node): void { return true; }); if (!~__cssModulesIndex) return; + /* This region assumeed that the entered node looks like the following code. + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]); + * ``` + */ + //#endregion + //#region const cssModuleForestName = ((node.declarations[0].init.arguments[1].elements[__cssModulesIndex] as estree.ArrayExpression).elements[1] as estree.Identifier).name; const cssModuleForestNode = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; @@ -117,6 +133,16 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (property.value.type !== 'Identifier') return []; return [[property.key.value as string, property.value.name as string]]; })); + /* This region collected a VariableDeclaration node in the module that looks like the following code. + * + * ```ts + * const cssModules = { + * "$style": style0, + * }; + * ``` + */ + //#endregion + //#region const sfcMain = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; if (x.declarations.length !== 1) return false; @@ -146,7 +172,22 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (ctx.type !== 'Identifier') return; if (ctx.name !== '_ctx') return; if (render.argument.body.type !== 'BlockStatement') return; + /* This region assumed that `sfcMain` looks like the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * }; + * }, + * }); + * ``` + */ + //#endregion for (const [key, value] of moduleForest) { + //#region const cssModuleTreeNode = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; if (x.declarations.length !== 1) return false; @@ -172,6 +213,19 @@ export function unwindCssModuleClassName(ast: estree.Node): void { if (actualValue.declarations[0].init?.type !== 'Literal') return []; return [[actualKey, actualValue.declarations[0].init.value as string]]; })); + /* This region collected VariableDeclaration nodes in the module that looks like the following code. + * + * ```ts + * const foo = "bar"; + * const baz = "qux"; + * const style0 = { + * foo: foo, + * baz: baz, + * }; + * ``` + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'MemberExpression') return; @@ -189,6 +243,39 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region inlined the reference identifier of the class name in the render function into the actual literal, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(_ctx.$style.foo), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass("bar"), + * }, null); + * }; + * }, + * }); + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'MemberExpression') return; @@ -205,13 +292,47 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region replaced the reference identifier of missing class names in the render function with `undefined`, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(_ctx.$style.hoge), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass(undefined), + * }, null); + * }; + * }, + * }); + * ``` + */ + //#endregion + //#region (walk as typeof estreeWalker.walk)(render.argument.body, { enter(childNode) { if (childNode.type !== 'CallExpression') return; if (childNode.callee.type !== 'Identifier') return; if (childNode.callee.name !== 'normalizeClass') return; if (childNode.arguments.length !== 1) return; - const normalized = normalizeClass(childNode.arguments[0]); + const normalized = normalizeClass(childNode.arguments[0], name); if (normalized === null) return; this.replace({ type: 'Literal', @@ -219,8 +340,60 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + /* This region compiled the `normalizeClass` call into a pseudo-AOT compilation, as in the following code. + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: normalizeClass("bar"), + * }, null); + * }; + * }, + * }); + * ``` + * + * ↓ + * + * ```ts + * const _sfc_main = defineComponent({ + * setup(_props) { + * ... + * return (_ctx, _cache) => { + * ... + * return openBlock(), createElementBlock("div", { + * class: "bar", + * }, null); + * }; + * }, + * }); + * ``` + */ + //#endregion } + //#region if (node.declarations[0].init.arguments[1].elements.length === 1) { + (walk as typeof estreeWalker.walk)(ast, { + enter(childNode) { + if (childNode.type !== 'Identifier') return; + if (childNode.name !== ident) return; + this.replace({ + type: 'Identifier', + name: node.declarations[0].id.name, + }); + }, + }); + this.remove(); + /* NOTE: The above logic is valid as long as the following two conditions are met. + * + * - the uniqueness of `ident` is kept throughout the module + * - `_export_sfc` is noop when the second argument is an empty array + * + * Otherwise, the below logic should be used instead. + this.replace({ type: 'VariableDeclaration', declarations: [{ @@ -236,6 +409,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }], kind: 'const', }); + */ } else { this.replace({ type: 'VariableDeclaration', @@ -263,6 +437,35 @@ export function unwindCssModuleClassName(ast: estree.Node): void { kind: 'const', }); } + /* This region removed the `__cssModules` reference from the second argument of `_export_sfc`, as in the following code. + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar], ["__cssModules", cssModules]]); + * ``` + * + * ↓ + * + * ```ts + * const SomeComponent = _export_sfc(_sfc_main, [["foo", bar]]); + * ``` + * + * When the declaration becomes noop, it is removed as follows. + * + * ```ts + * const _sfc_main = defineComponent({ + * ... + * }); + * const SomeComponent = _export_sfc(_sfc_main, []); + * ``` + * + * ↓ + * + * ```ts + * const SomeComponent = defineComponent({ + * ... + * }); + */ + //#endregion }, }); } diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 03788af21e..60f16f285f 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]"> +<div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]"> <div :class="$style.codeEditorScroller"> <textarea ref="inputEl" diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index a8ccf26c4c..48d90522c4 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -145,7 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button> </div> <div> - <div v-if="tab === 'replies'" :class="$style.tab_replies"> + <div v-if="tab === 'replies'"> <div v-if="!repliesLoaded" style="padding: 16px"> <MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton> </div> diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index 8e946e7437..2e2c0e15a2 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"> +<div :class="[$style.root, { [$style.disabled]: disabled }]"> <input ref="input" type="checkbox" @@ -64,9 +64,6 @@ const toggle = () => { opacity: 0.6; cursor: not-allowed; } - - //&.checked { - //} } .input { diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 55ad1e2ab1..4ad8cc58c5 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -134,10 +134,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'roles'" class="_gaps"> <MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> - <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> + <div v-for="role in info.roles" :key="role.id"> <div :class="$style.roleItemMain"> <MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> - <button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button> + <button class="_button" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button> <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> </div> @@ -621,9 +621,6 @@ definePageMetadata(computed(() => ({ } } -.roleItem { -} - .roleItemMain { display: flex; } diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index bceefcf6c8..fe825613fa 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTime :time="log.createdAt"/> </template> - <div :class="$style.root"> + <div> <div style="display: flex; gap: var(--margin); flex-wrap: wrap;"> <div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div> <div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div> @@ -134,9 +134,6 @@ const props = defineProps<{ </script> <style lang="scss" module> -.root { -} - .avatar { width: 18px; height: 18px; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 10a073243b..3e3e2b949c 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XSidebar v-if="!isMobile"/> <div :class="$style.main"> - <XAnnouncements v-if="$i" :class="$style.announcements"/> + <XAnnouncements v-if="$i"/> <XStatusBars/> <div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel"> <!-- sectionを利用ã—ã¦ã„ã‚‹ã®ã¯ã€deck.vueå´ã§columnã«å¯¾ã—ã¦first-of-typeを効ã‹ã›ã‚‹ãŸã‚ --> diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index cba7b82610..f46f55d988 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu"> <template #header> <div> - <XAnnouncements v-if="$i" :class="$style.announcements"/> + <XAnnouncements v-if="$i"/> <XStatusBars :class="$style.statusbars"/> </div> </template> -- GitLab