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 456d04e743..b3f0d70ea6 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 @@ -3,7 +3,7 @@ import { generate } from 'astring'; import { expect, it } from 'vitest'; import { unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name'; -it('Composition API', () => { +it('Composition API (standard)', () => { 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'; @@ -214,3 +214,336 @@ const index_photos = _sfc_main; export {index_photos as default}; `.slice(1)); }); + +it('Composition API (with `useCssModule()`)', () => { + const ast = parse(` +import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js'; +import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js'; + +function isDebuggerEnabled(id) { + try { + return localStorage.getItem(\`DEBUG_\${id}\`) !== null; + } catch { + return false; + } +} +function stackTraceInstances() { + let instance = getCurrentInstance(); + const stack = []; + while (instance) { + stack.push(instance); + instance = instance.parent; + } + return stack; +} + +const _sfc_main = defineComponent({ + props: { + items: { + type: Array, + required: true + }, + direction: { + type: String, + required: false, + default: "down" + }, + reversed: { + type: Boolean, + required: false, + default: false + }, + noGap: { + type: Boolean, + required: false, + default: false + }, + ad: { + type: Boolean, + required: false, + default: false + } + }, + setup(props, { slots, expose }) { + const $style = useCssModule(); + function getDateText(time) { + const date = new Date(time).getDate(); + const month = new Date(time).getMonth() + 1; + return i18n.t("monthAndDay", { + month: month.toString(), + day: date.toString() + }); + } + if (props.items.length === 0) + return; + const renderChildrenImpl = () => props.items.map((item, i) => { + if (!slots || !slots.default) + return; + const el = slots.default({ + item + })[0]; + if (el.key == null && item.id) + el.key = item.id; + if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) { + const separator = h("div", { + class: $style["separator"], + key: item.id + ":separator" + }, h("p", { + class: $style["date"] + }, [ + h("span", { + class: $style["date-1"] + }, [ + h("i", { + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` + }), + getDateText(item.createdAt) + ]), + h("span", { + class: $style["date-2"] + }, [ + getDateText(props.items[i + 1].createdAt), + h("i", { + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` + }) + ]) + ])); + return [el, separator]; + } else { + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ":ad", + prefer: ["horizontal", "horizontal-big"] + }), el]; + } else { + return el; + } + } + }); + const renderChildren = () => { + const children = renderChildrenImpl(); + if (isDebuggerEnabled(6864)) { + const nodes = children.flatMap((node) => node ?? []); + const keys = new Set(nodes.map((node) => node.key)); + if (keys.size !== nodes.length) { + const id = crypto.randomUUID(); + const instances = stackTraceInstances(); + toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`)); + console.warn({ id, debugId: 6864, stack: instances }); + } + } + return children; + }; + function onBeforeLeave(el) { + el.style.top = \`\${el.offsetTop}px\`; + el.style.left = \`\${el.offsetLeft}px\`; + } + function onLeaveCanceled(el) { + el.style.top = ""; + el.style.left = ""; + } + return () => h( + defaultStore.state.animation ? TransitionGroup : "div", + { + class: { + [$style["date-separated-list"]]: true, + [$style["date-separated-list-nogap"]]: props.noGap, + [$style["reversed"]]: props.reversed, + [$style["direction-down"]]: props.direction === "down", + [$style["direction-up"]]: props.direction === "up" + }, + ...defaultStore.state.animation ? { + name: "list", + tag: "div", + onBeforeLeave, + onLeaveCanceled + } : {} + }, + { default: renderChildren } + ); + } +}); + +const reversed = "xxiZh"; +const separator = "xxeDx"; +const date = "xxawD"; +const style0 = { + "date-separated-list": "xfKPa", + "date-separated-list-nogap": "xf9zr", + "direction-up": "x7AeO", + "direction-down": "xBIqc", + reversed: reversed, + separator: separator, + date: date, + "date-1": "xwtmh", + "date-1-icon": "xsNPa", + "date-2": "x1xvw", + "date-2-icon": "x9ZiG" +}; + +const cssModules = { + "$style": style0 +}; +const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]); + +export { MkDateSeparatedList as M }; +`.slice(1), { sourceType: 'module' }); + unwindCssModuleClassName(ast); + expect(generate(ast)).toBe(` +import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js'; +import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js'; +function isDebuggerEnabled(id) { + try { + return localStorage.getItem(\`DEBUG_\${id}\`) !== null; + } catch { + return false; + } +} +function stackTraceInstances() { + let instance = getCurrentInstance(); + const stack = []; + while (instance) { + stack.push(instance); + instance = instance.parent; + } + return stack; +} +const _sfc_main = defineComponent({ + props: { + items: { + type: Array, + required: true + }, + direction: { + type: String, + required: false, + default: "down" + }, + reversed: { + type: Boolean, + required: false, + default: false + }, + noGap: { + type: Boolean, + required: false, + default: false + }, + ad: { + type: Boolean, + required: false, + default: false + } + }, + setup(props, {slots, expose}) { + const $style = useCssModule(); + function getDateText(time) { + const date = new Date(time).getDate(); + const month = new Date(time).getMonth() + 1; + return i18n.t("monthAndDay", { + month: month.toString(), + day: date.toString() + }); + } + if (props.items.length === 0) return; + const renderChildrenImpl = () => props.items.map((item, i) => { + if (!slots || !slots.default) return; + const el = slots.default({ + item + })[0]; + if (el.key == null && item.id) el.key = item.id; + if (i !== props.items.length - 1 && new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()) { + const separator = h("div", { + class: $style["separator"], + key: item.id + ":separator" + }, h("p", { + class: $style["date"] + }, [h("span", { + class: $style["date-1"] + }, [h("i", { + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` + }), getDateText(item.createdAt)]), h("span", { + class: $style["date-2"] + }, [getDateText(props.items[i + 1].createdAt), h("i", { + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` + })])])); + return [el, separator]; + } else { + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ":ad", + prefer: ["horizontal", "horizontal-big"] + }), el]; + } else { + return el; + } + } + }); + const renderChildren = () => { + const children = renderChildrenImpl(); + if (isDebuggerEnabled(6864)) { + const nodes = children.flatMap(node => node ?? []); + const keys = new Set(nodes.map(node => node.key)); + if (keys.size !== nodes.length) { + const id = crypto.randomUUID(); + const instances = stackTraceInstances(); + toast(instances.reduce((a, c) => \`\${a} at \${c.type.name}\`, \`[DEBUG_6864 (\${id})]: \${nodes.length - keys.size} duplicated keys found\`)); + console.warn({ + id, + debugId: 6864, + stack: instances + }); + } + } + return children; + }; + function onBeforeLeave(el) { + el.style.top = \`\${el.offsetTop}px\`; + el.style.left = \`\${el.offsetLeft}px\`; + } + function onLeaveCanceled(el) { + el.style.top = ""; + el.style.left = ""; + } + return () => h(defaultStore.state.animation ? TransitionGroup : "div", { + class: { + [$style["date-separated-list"]]: true, + [$style["date-separated-list-nogap"]]: props.noGap, + [$style["reversed"]]: props.reversed, + [$style["direction-down"]]: props.direction === "down", + [$style["direction-up"]]: props.direction === "up" + }, + ...defaultStore.state.animation ? { + name: "list", + tag: "div", + onBeforeLeave, + onLeaveCanceled + } : {} + }, { + default: renderChildren + }); + } +}); +const reversed = "xxiZh"; +const separator = "xxeDx"; +const date = "xxawD"; +const style0 = { + "date-separated-list": "xfKPa", + "date-separated-list-nogap": "xf9zr", + "direction-up": "x7AeO", + "direction-down": "xBIqc", + reversed: reversed, + separator: separator, + date: date, + "date-1": "xwtmh", + "date-1-icon": "xsNPa", + "date-2": "x1xvw", + "date-2-icon": "x9ZiG" +}; +const cssModules = { + "$style": style0 +}; +const MkDateSeparatedList = _export_sfc(_sfc_main, [["__cssModules", cssModules]]); +export {MkDateSeparatedList as M}; +`.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 5d188adb33..2992d3119a 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 @@ -7,49 +7,25 @@ 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', - }; + if (node.type !== 'VariableDeclaration') return; + if (node.declarations.length !== 1) return; + if (node.declarations[0].id.type !== 'Identifier') return; + if (node.declarations[0].init?.type !== 'CallExpression') return; + if (node.declarations[0].init.callee.type !== 'Identifier') return; + if (node.declarations[0].init.callee.name !== '_export_sfc') return; + if (node.declarations[0].init.arguments.length !== 2) return; + if (node.declarations[0].init.arguments[0].type !== 'Identifier') return; + const ident = node.declarations[0].init.arguments[0].name; + if (!ident.startsWith('_sfc_main')) return; + if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return; + if (node.declarations[0].init.arguments[1].elements.length !== 1) return; + if (node.declarations[0].init.arguments[1].elements[0]?.type !== 'ArrayExpression') return; + if (node.declarations[0].init.arguments[1].elements[0].elements.length !== 2) return; + if (node.declarations[0].init.arguments[1].elements[0].elements[0]?.type !== 'Literal') return; + if (node.declarations[0].init.arguments[1].elements[0].elements[0].value !== '__cssModules') return; + if (node.declarations[0].init.arguments[1].elements[0].elements[1]?.type !== 'Identifier') return; + const cssModuleForestName = node.declarations[0].init.arguments[1].elements[0].elements[1].name; const cssModuleForestNode = parent.body.find((x) => { if (x.type !== 'VariableDeclaration') return false; if (x.declarations.length !== 1) return false; @@ -68,7 +44,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void { 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; + if (x.declarations[0].id.name !== ident) return false; return true; }) as unknown as estree.VariableDeclaration; if (sfcMain.declarations[0].init?.type !== 'CallExpression') return; @@ -88,8 +64,10 @@ export function unwindCssModuleClassName(ast: estree.Node): void { return true; }) as unknown as estree.ReturnStatement; if (render.argument?.type !== 'ArrowFunctionExpression') return; + if (render.argument.params.length !== 2) return; const ctx = render.argument.params[0]; if (ctx.type !== 'Identifier') return; + if (ctx.name !== '_ctx') return; if (render.argument.body.type !== 'BlockStatement') return; //console.dir(render, { depth: Infinity }); for (const [key, value] of moduleForest) { @@ -133,6 +111,21 @@ export function unwindCssModuleClassName(ast: estree.Node): void { }); }, }); + this.replace({ + type: 'VariableDeclaration', + declarations: [{ + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: node.declarations[0].id.name, + }, + init: { + type: 'Identifier', + name: ident, + }, + }], + kind: 'const', + }); } }, }); @@ -143,12 +136,8 @@ 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) }; }, };