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