perf(): unwind css module class name

This commit is contained in:
Acid Chicken (硫酸鶏) 2023-05-31 04:10:04 +09:00
parent f8f3304164
commit 172f301df4
No known key found for this signature in database
GPG key ID: 3E87B98A3F6BAB99
5 changed files with 1602 additions and 186 deletions

View file

@ -0,0 +1,216 @@
import { parse } from 'acorn';
import { generate } from 'astring';
import { expect, it } from 'vitest';
import { unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name';
it('Composition API', () => {
const ast = parse(`
import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
import { M as MkContainer } from './MkContainer-!~{03M}~.js';
import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
import './photoswipe-!~{003}~.js';
const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
const _sfc_main = /* @__PURE__ */ defineComponent({
__name: "index.photos",
props: {
user: {}
},
setup(__props) {
const props = __props;
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = [
"image/jpeg",
"image/webp",
"image/avif",
"image/png",
"image/gif",
"image/apng",
"image/vnd.mozilla.apng"
];
api("users/notes", {
userId: props.user.id,
fileType: image,
excludeNsfw: defaultStore.state.nsfw !== "ignore",
limit: 10
}).then((notes) => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
note,
file
});
}
}
fetching.value = false;
});
});
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
return openBlock(), createBlock(MkContainer, {
"max-height": 300,
foldable: true
}, {
icon: withCtx(() => [
_hoisted_1
]),
header: withCtx(() => [
createTextVNode(toDisplayString(unref(i18n).ts.images), 1)
]),
default: withCtx(() => [
createBaseVNode("div", {
class: normalizeClass(_ctx.$style.root)
}, [
unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true),
!unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: normalizeClass(_ctx.$style.stream)
}, [
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => {
return openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: normalizeClass(_ctx.$style.img),
to: unref(notePage)(image.note)
}, {
default: withCtx(() => [
createVNode(ImgWithBlurhash, {
hash: image.file.blurhash,
src: thumbnail(image.file),
title: image.file.name
}, null, 8, ["hash", "src", "title"])
]),
_: 2
}, 1032, ["class", "to"]);
}), 128))
], 2)) : createCommentVNode("", true),
!unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
key: 2,
class: normalizeClass(_ctx.$style.empty)
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)
], 2)
]),
_: 1
});
};
}
});
const root = "xenMW";
const stream = "xaZzf";
const img = "xtA8t";
const empty = "xhYKj";
const style0 = {
root: root,
stream: stream,
img: img,
empty: empty
};
const cssModules = {
"$style": style0
};
const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { index_photos as default };
`.slice(1), { sourceType: 'module' });
unwindCssModuleClassName(ast);
expect(generate(ast)).toBe(`
import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
import {M as MkContainer} from './MkContainer-!~{03M}~.js';
import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
import './photoswipe-!~{003}~.js';
const _hoisted_1 = createBaseVNode("i", {
class: "ti ti-photo"
}, null, -1);
const _sfc_main = defineComponent({
__name: "index.photos",
props: {
user: {}
},
setup(__props) {
const props = __props;
let fetching = ref(true);
let images = ref([]);
function thumbnail(image) {
return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
}
onMounted(() => {
const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
api("users/notes", {
userId: props.user.id,
fileType: image,
excludeNsfw: defaultStore.state.nsfw !== "ignore",
limit: 10
}).then(notes => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
note,
file
});
}
}
fetching.value = false;
});
});
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
return (openBlock(), createBlock(MkContainer, {
"max-height": 300,
foldable: true
}, {
icon: withCtx(() => [_hoisted_1]),
header: withCtx(() => [createTextVNode(toDisplayString(unref(i18n).ts.images), 1)]),
default: withCtx(() => [createBaseVNode("div", {
class: normalizeClass("xenMW")
}, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, {
key: 0
})) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: normalizeClass("xaZzf")
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => {
return (openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: normalizeClass("xtA8t"),
to: unref(notePage)(image.note)
}, {
default: withCtx(() => [createVNode(ImgWithBlurhash, {
hash: image.file.blurhash,
src: thumbnail(image.file),
title: image.file.name
}, null, 8, ["hash", "src", "title"])]),
_: 2
}, 1032, ["class", "to"]));
}), 128))], 2)) : createCommentVNode("", true), !unref(fetching) && unref(images).length == 0 ? (openBlock(), createElementBlock("p", {
key: 2,
class: normalizeClass("xhYKj")
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]),
_: 1
}));
};
}
});
const root = "xenMW";
const stream = "xaZzf";
const img = "xtA8t";
const empty = "xhYKj";
const style0 = {
root: root,
stream: stream,
img: img,
empty: empty
};
const cssModules = {
"$style": style0
};
const index_photos = _sfc_main;
export {index_photos as default};
`.slice(1));
});

View file

@ -0,0 +1,155 @@
import { generate } from 'astring';
import * as estree from 'estree';
import { walk } from '../node_modules/estree-walker/src/index.js';
import type * as estreeWalker from 'estree-walker';
import type { Plugin } from 'vite';
export function unwindCssModuleClassName(ast: estree.Node): void {
(walk as typeof estreeWalker.walk)(ast, {
enter(node, parent): void {
// FIXME: support multiple exports
if (node.type !== 'ExportNamedDeclaration') return;
if (node.specifiers.length !== 1) return;
if (node.specifiers[0].local.name === '_sfc_main') return;
if (node.specifiers[0].exported.name !== 'default') return;
if (parent?.type !== 'Program') return;
const endIndex = parent.body.indexOf(node);
const previousNode = parent.body[endIndex - 1];
if (previousNode.type !== 'VariableDeclaration') return;
if (previousNode.declarations.length !== 1) return;
if (previousNode.declarations[0].id.type !== 'Identifier') return;
if (previousNode.declarations[0].id.name !== node.specifiers[0].local.name) return;
if (previousNode.declarations[0].init?.type !== 'CallExpression') return;
if (previousNode.declarations[0].init.callee.type !== 'Identifier') return;
if (previousNode.declarations[0].init.callee.name !== '_export_sfc') return;
if (previousNode.declarations[0].init.arguments.length !== 2) return;
if (previousNode.declarations[0].init.arguments[0].type !== 'Identifier') return;
if (previousNode.declarations[0].init.arguments[0].name !== '_sfc_main') return;
if (previousNode.declarations[0].init.arguments[1].type !== 'ArrayExpression') return;
if (previousNode.declarations[0].init.arguments[1].elements.length !== 1) return;
if (previousNode.declarations[0].init.arguments[1].elements[0]?.type !== 'ArrayExpression') return;
if (previousNode.declarations[0].init.arguments[1].elements[0].elements.length !== 2) return;
if (previousNode.declarations[0].init.arguments[1].elements[0].elements[0]?.type !== 'Literal') return;
if (previousNode.declarations[0].init.arguments[1].elements[0].elements[0].value !== '__cssModules') return;
if (previousNode.declarations[0].init.arguments[1].elements[0].elements[1]?.type !== 'Identifier') return;
const cssModuleForestName = previousNode.declarations[0].init.arguments[1].elements[0].elements[1].name;
parent.body[endIndex - 1] = {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: node.specifiers[0].local.name,
},
init: {
type: 'Identifier',
name: '_sfc_main',
},
},
],
kind: 'const',
};
const cssModuleForestNode = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== cssModuleForestName) return false;
if (x.declarations[0].init?.type !== 'ObjectExpression') return false;
return true;
}) as unknown as estree.VariableDeclaration;
const moduleForest = new Map((cssModuleForestNode.declarations[0].init as estree.ObjectExpression).properties.flatMap((property) => {
if (property.type !== 'Property') return [];
if (property.key.type !== 'Literal') return [];
if (property.value.type !== 'Identifier') return [];
return [[property.key.value as string, property.value.name as string]];
}));
const sfcMain = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== '_sfc_main') return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (sfcMain.declarations[0].init?.type !== 'CallExpression') return;
if (sfcMain.declarations[0].init.callee.type !== 'Identifier') return;
if (sfcMain.declarations[0].init.callee.name !== 'defineComponent') return;
if (sfcMain.declarations[0].init.arguments.length !== 1) return;
if (sfcMain.declarations[0].init.arguments[0].type !== 'ObjectExpression') return;
const setup = sfcMain.declarations[0].init.arguments[0].properties.find((x) => {
if (x.type !== 'Property') return false;
if (x.key.type !== 'Identifier') return false;
if (x.key.name !== 'setup') return false;
return true;
}) as unknown as estree.Property;
if (setup.value.type !== 'FunctionExpression') return;
const render = setup.value.body.body.find((x) => {
if (x.type !== 'ReturnStatement') return false;
return true;
}) as unknown as estree.ReturnStatement;
if (render.argument?.type !== 'ArrowFunctionExpression') return;
const ctx = render.argument.params[0];
if (ctx.type !== 'Identifier') return;
if (render.argument.body.type !== 'BlockStatement') return;
//console.dir(render, { depth: Infinity });
for (const [key, value] of moduleForest) {
const cssModuleTreeNode = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== value) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (cssModuleTreeNode.declarations[0].init?.type !== 'ObjectExpression') return;
const moduleTree = new Map(cssModuleTreeNode.declarations[0].init.properties.flatMap((property) => {
if (property.type !== 'Property') return [];
if (property.key.type !== 'Identifier') return [];
if (property.value.type !== 'Identifier') return [];
const labelledValue = property.value.name;
const actualValue = parent.body.find((x) => {
if (x.type !== 'VariableDeclaration') return false;
if (x.declarations.length !== 1) return false;
if (x.declarations[0].id.type !== 'Identifier') return false;
if (x.declarations[0].id.name !== labelledValue) return false;
return true;
}) as unknown as estree.VariableDeclaration;
if (actualValue.declarations[0].init?.type !== 'Literal') return [];
return [[property.key.name, actualValue.declarations[0].init.value as string]];
}));
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
if (childNode.type !== 'MemberExpression') return;
if (childNode.object.type !== 'MemberExpression') return;
if (childNode.object.object.type !== 'Identifier') return;
if (childNode.object.object.name !== ctx.name) return;
if (childNode.object.property.type !== 'Identifier') return;
if (childNode.object.property.name !== key) return;
if (childNode.property.type !== 'Identifier') return;
const actualValue = moduleTree.get(childNode.property.name);
if (actualValue === undefined) return;
this.replace({
type: 'Literal',
value: actualValue,
});
},
});
}
},
});
}
// eslint-disable-next-line import/no-default-export
export default function pluginUnwindCssModuleClassName(): Plugin {
return {
name: 'UnwindCssModuleClassName',
renderChunk(code, chunk): { code: string } {
console.log(`=======${chunk.fileName} BEFORE=======`);
console.log(code);
const ast = this.parse(code) as unknown as estree.Node;
unwindCssModuleClassName(ast);
console.log(`=======${chunk.fileName} AFTER=======`);
console.log(generate(ast));
return { code: generate(ast) };
},
};
}

View file

@ -24,6 +24,7 @@
"@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "0.3.8",
"@vue/compiler-sfc": "3.3.4",
"astring": "1.8.5",
"autosize": "6.0.1",
"broadcast-channel": "4.20.2",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
@ -39,6 +40,7 @@
"cropperjs": "2.0.0-beta.2",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
"estree-walker": "^3.0.3",
"eventemitter3": "5.0.1",
"gsap": "3.11.5",
"idb-keyval": "6.2.1",
@ -76,6 +78,7 @@
"vuedraggable": "next"
},
"devDependencies": {
"@babel/types": "^7.22.4",
"@storybook/addon-actions": "7.0.18",
"@storybook/addon-essentials": "7.0.18",
"@storybook/addon-interactions": "7.0.18",
@ -96,6 +99,7 @@
"@storybook/vue3-vite": "7.0.18",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/vue": "7.0.0",
"@types/babel__traverse": "^7.20.0",
"@types/escape-regexp": "0.0.1",
"@types/estree": "1.0.1",
"@types/gulp": "4.0.10",
@ -116,7 +120,7 @@
"@typescript-eslint/parser": "5.59.5",
"@vitest/coverage-c8": "0.31.1",
"@vue/runtime-core": "3.3.4",
"astring": "1.8.5",
"acorn": "^8.8.2",
"chokidar-cli": "3.0.0",
"cross-env": "7.0.3",
"cypress": "12.13.0",

View file

@ -8,6 +8,7 @@ import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
import locales from '../../locales';
import generateDTS from '../../locales/generateDTS';
import meta from '../../package.json';
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name';
import pluginJson5 from './vite.json5';
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
@ -54,6 +55,7 @@ export function getConfig(): UserConfig {
reactivityTransform: true,
}),
ReactivityTransform(),
pluginUnwindCssModuleClassName(),
pluginJson5(),
...process.env.NODE_ENV === 'production'
? [

File diff suppressed because it is too large Load diff