1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-13 16:25:44 +02:00

enhance(frontend): update vite to v8 再 (#17289)

* Revert "Revert "deps: Update vite to v8" (#17283)"

This reverts commit a18c909ba3.

* fix(frontend): popupのりアクティビティがチャンクをまたいで切れる事がある問題を修正

* update vite/rolldown
This commit is contained in:
かっこかり
2026-04-09 14:20:07 +09:00
committed by GitHub
parent 92e0e8edf7
commit 4750980cef
21 changed files with 1456 additions and 635 deletions

View File

@@ -228,6 +228,6 @@
"pid-port": "2.1.0",
"simple-oauth2": "5.1.0",
"supertest": "7.2.2",
"vite": "7.3.1"
"vite": "8.0.7"
}
}

View File

@@ -3,10 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parseAst } from 'vite';
import { parseAst } from 'rolldown/parseAst';
import * as estreeWalker from 'estree-walker';
import { assertNever, assertType } from '../utils.js';
import type { AstNode, ProgramNode } from 'rollup';
import type { ESTree as RolldownESTree } from 'rolldown/utils';
import type { AstNode } from 'rollup';
import type * as estree from 'estree';
import type { LocaleInliner, TextModification } from '../locale-inliner.js';
import type { Logger } from '../logger.js';
@@ -17,7 +18,7 @@ interface WalkerContext {
}
export function collectModifications(sourceCode: string, fileName: string, fileLogger: Logger, inliner: LocaleInliner): TextModification[] {
let programNode: ProgramNode;
let programNode: RolldownESTree.Program;
try {
programNode = parseAst(sourceCode);
} catch (err) {
@@ -35,7 +36,8 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
// 1) replace all `scripts/` path literals with locale code
// 2) replace all `localStorage.getItem("lang")` with `localeName` variable
// 3) replace all `await window.fetch(`/assets/locales/${d}.${x}.json`).then(u=>u.json())` with `localeJson` variable
estreeWalker.walk(programNode, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(estreeWalker.walk as any)(programNode, {
enter(this: WalkerContext, node: Node) {
assertType<AstNode>(node);
@@ -118,8 +120,9 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
// Check if the identifier is already declared in the file.
// If it is, we may overwrite it and cause issues so we skip inlining
let isSupported = true;
estreeWalker.walk(programNode, {
enter(node) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(estreeWalker.walk as any)(programNode, {
enter(node: Node) {
if (node.type === 'VariableDeclaration') {
assertType<estree.VariableDeclaration>(node);
for (const id of node.declarations.flatMap(x => declsOfPattern(x.id))) {
@@ -145,8 +148,9 @@ export function collectModifications(sourceCode: string, fileName: string, fileL
const toSkip = new Set();
toSkip.add(i18nImport);
estreeWalker.walk(programNode, {
enter(this: WalkerContext, node, parent, property) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(estreeWalker.walk as any)(programNode, {
enter(this: WalkerContext, node: Node, parent: Node | null, property: string | number | symbol | null | undefined) {
assertType<AstNode>(node);
assertType<AstNode>(parent);
if (toSkip.has(node)) {
@@ -379,7 +383,7 @@ type SpecifierResult =
| { type: 'specifier', localI18nIdentifier: string, importNode: estree.ImportDeclaration & AstNode }
;
function findImportSpecifier(programNode: ProgramNode, i18nFileName: string, i18nSymbol: string): SpecifierResult {
function findImportSpecifier(programNode: RolldownESTree.Program, i18nFileName: string, i18nSymbol: string): SpecifierResult {
const imports = programNode.body.filter(x => x.type === 'ImportDeclaration');
const importNode = imports.find(x => x.source.value === `./${i18nFileName}`) as estree.ImportDeclaration | undefined;
if (!importNode) return { type: 'no-import' };

View File

@@ -17,9 +17,10 @@
"rollup": "4.60.0"
},
"dependencies": {
"i18n": "workspace:*",
"estree-walker": "3.0.3",
"i18n": "workspace:*",
"magic-string": "0.30.21",
"vite": "7.3.1"
"rolldown": "1.0.0-rc.13",
"vite": "8.0.7"
}
}

View File

@@ -4,11 +4,11 @@
*/
import * as estreeWalker from 'estree-walker';
import MagicString from 'magic-string';
import { RolldownMagicString } from 'rolldown';
import { assertType } from './utils.js';
import type { ESTree } from 'rolldown/utils';
import type { Plugin } from 'vite';
import type { CallExpression, Expression, Program } from 'estree';
import type { AstNode } from 'rollup';
import type { CallExpression, Expression } from 'estree';
// This plugin transforms `unref(i18n)` to `i18n` in the code, which is useful for removing unnecessary unref calls
// and helps locale inliner runs after vite build to inline the locale data into the final build.
@@ -23,12 +23,13 @@ export function pluginRemoveUnrefI18n(
} = {}): Plugin {
return {
name: 'UnwindCssModuleClassName',
renderChunk(code) {
renderChunk(code, _chunk, _options, meta) {
if (!code.includes('unref(i18n)')) return null;
const ast = this.parse(code) as Program;
const magicString = new MagicString(code);
estreeWalker.walk(ast, {
enter(node) {
const ast = this.parse(code);
const magicString = meta.magicString ?? new RolldownMagicString(code);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(estreeWalker.walk as any)(ast, {
enter(node: ESTree.Node) {
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'unref'
&& node.arguments.length === 1) {
// calls to unref with single argument
@@ -36,18 +37,16 @@ export function pluginRemoveUnrefI18n(
if (arg.type === 'Identifier' && arg.name === i18nSymbolName) {
// this is unref(i18n) so replace it with i18n
// to replace, remove the 'unref(' and the trailing ')'
assertType<CallExpression & AstNode>(node);
assertType<Expression & AstNode>(arg);
assertType<CallExpression>(node);
assertType<Expression>(arg);
magicString.remove(node.start, arg.start);
magicString.remove(arg.end, node.end);
}
}
},
});
return {
code: magicString.toString(),
map: magicString.generateMap({ hires: true }),
};
return magicString;
},
};
}

View File

@@ -3,7 +3,7 @@ import url from 'node:url';
import path from 'node:path';
import { execa } from 'execa';
import locales from 'i18n';
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
import { LocaleInliner } from '../frontend-builder/locale-inliner.js';
import { createLogger } from '../frontend-builder/logger';
// requires node 21 or later

View File

@@ -1,7 +1,10 @@
// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import JSON5 from 'json5';
import { Plugin } from 'rollup';
import { Plugin } from 'vite';
import { createFilter, dataToEsm } from '@rollup/pluginutils';
import { RollupJsonOptions } from '@rollup/plugin-json';

View File

@@ -12,7 +12,6 @@
"dependencies": {
"@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.5",
@@ -26,11 +25,9 @@
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.60.0",
"sass": "1.98.0",
"shiki": "3.23.0",
"tinycolor2": "1.6.0",
"uuid": "13.0.0",
"vite": "7.3.1",
"vue": "3.5.30"
},
"devDependencies": {
@@ -45,7 +42,7 @@
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/coverage-v8": "4.1.1",
"@vitest/coverage-v8": "4.1.2",
"@vue/runtime-core": "3.5.30",
"acorn": "8.16.0",
"cross-env": "10.1.0",
@@ -57,8 +54,10 @@
"msw": "2.12.14",
"nodemon": "3.1.14",
"prettier": "3.8.1",
"sass-embedded": "1.98.0",
"start-server-and-test": "2.1.5",
"tsx": "4.21.0",
"vite": "8.0.7",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.2.6",
"vue-eslint-parser": "10.4.0",

View File

@@ -7,10 +7,10 @@ import { promises as fsp } from 'fs';
import locales from 'i18n';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
import pluginJson5 from './vite.json5.js';
import pluginJson5 from './lib/vite-plugin-json5.js';
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n';
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
const url = process.env.NODE_ENV === 'development' ? (yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')) as any).url : null;
const host = url ? (new URL(url)).hostname : undefined;
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
@@ -113,11 +113,6 @@ export function getConfig(): UserConfig {
}
},
},
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
},
define: {
@@ -137,7 +132,10 @@ export function getConfig(): UserConfig {
'safari16',
],
manifest: 'manifest.json',
rollupOptions: {
rolldownOptions: {
experimental: {
nativeMagicString: true,
},
input: {
i18n: './src/i18n.ts',
entry: './src/boot.ts',
@@ -145,10 +143,15 @@ export function getConfig(): UserConfig {
external: externalPackages.map(p => p.match),
preserveEntrySignatures: 'allow-extension',
output: {
manualChunks: {
vue: ['vue'],
// dependencies of i18n.ts
'config': ['@@/js/config.js'],
codeSplitting: {
groups: [{
name: 'vue',
test: /node_modules[\\/]vue/,
}, {
// dependencies of i18n.ts
name: 'config',
test: /@@[\\/]js[\\/]config\.js/,
}],
},
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,

View File

@@ -3,15 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parse } from 'acorn';
import { generate } from 'astring';
import { describe, expect, it } from 'vitest';
import { normalizeClass, unwindCssModuleClassName } from './rollup-plugin-unwind-css-module-class-name.js';
import type * as estree from 'estree';
import { parseAst } from 'rolldown/parseAst';
import type { ESTree } from 'rolldown/utils';
import { RolldownMagicString } from 'rolldown';
function parseExpression(code: string): estree.Expression {
const program = parse(code, { ecmaVersion: 'latest', sourceType: 'module' }) as unknown as estree.Program;
const statement = program.body[0] as estree.ExpressionStatement;
function parseExpression(code: string): ESTree.Expression {
const program = parseAst(code, { sourceType: 'module' });
const statement = program.body[0] as ESTree.ExpressionStatement;
return statement.expression;
}
@@ -57,7 +57,7 @@ describe(normalizeClass.name, () => {
});
it('Composition API (standard)', () => {
const ast = parse(`
const code = `
import { c as api, d as store, 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';
@@ -170,17 +170,19 @@ const cssModules = {
const index_photos = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { index_photos as default };
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
unwindCssModuleClassName(ast);
expect(generate(ast)).toBe(`
import {c as api, d as store, 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';
`.slice(1);
const ast = parseAst(code, { sourceType: 'module' });
const magicString = new RolldownMagicString(code);
unwindCssModuleClassName(ast, magicString);
expect(magicString.toString()).toBe(
`
import { c as api, d as store, 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 index_photos = defineComponent({
const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1);
const index_photos = /* @__PURE__ */ defineComponent({
__name: "index.photos",
props: {
user: {}
@@ -193,12 +195,20 @@ const index_photos = defineComponent({
return store.s.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"];
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,
limit: 10
}).then(notes => {
}).then((notes) => {
for (const note of notes) {
for (const file of note.files) {
images.value.push({
@@ -213,60 +223,77 @@ const index_photos = defineComponent({
return (_ctx, _cache) => {
const _component_MkLoading = resolveComponent("MkLoading");
const _component_MkA = resolveComponent("MkA");
return (openBlock(), createBlock(MkContainer, {
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: "xenMW"
}, [unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, {
key: 0
})) : createCommentVNode("", true), !unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: "xaZzf"
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), image => {
return (openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: "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: "xhYKj"
}, toDisplayString(unref(i18n).ts.nothing), 3)) : createCommentVNode("", true)], 2)]),
icon: withCtx(() => [
_hoisted_1
]),
header: withCtx(() => [
createTextVNode(toDisplayString(unref(i18n).ts.images), 1)
]),
default: withCtx(() => [
createBaseVNode("div", {
class: "xenMW"
}, [
unref(fetching) ? (openBlock(), createBlock(_component_MkLoading, { key: 0 })) : createCommentVNode("", true),
!unref(fetching) && unref(images).length > 0 ? (openBlock(), createElementBlock("div", {
key: 1,
class: "xaZzf"
}, [
(openBlock(true), createElementBlock(Fragment, null, renderList(unref(images), (image) => {
return openBlock(), createBlock(_component_MkA, {
key: image.note.id + image.file.id,
class: "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: "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
root: root,
stream: stream,
img: img,
empty: empty
};
const cssModules = {
"$style": style0
};
export {index_photos as default};
`.slice(1));
export { index_photos as default };
`.slice(1),
);
});
it('Composition API (with `useCssModule()`)', () => {
const ast = parse(`
const code = `
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
@@ -437,11 +464,15 @@ const cssModules = {
const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { MkDateSeparatedList as M };
`.slice(1), { ecmaVersion: 'latest', 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 store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
`.slice(1);
const ast = parseAst(code, { sourceType: 'module' });
const magicString = new RolldownMagicString(code);
unwindCssModuleClassName(ast, magicString);
expect(magicString.toString()).toBe(
`
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
import { d as store, 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;
@@ -458,6 +489,7 @@ function stackTraceInstances() {
}
return stack;
}
const _sfc_main = defineComponent({
props: {
items: {
@@ -485,7 +517,7 @@ const _sfc_main = defineComponent({
default: false
}
},
setup(props, {slots, expose}) {
setup(props, { slots, expose }) {
const $style = useCssModule();
function getDateText(time) {
const date = new Date(time).getDate();
@@ -495,28 +527,40 @@ const _sfc_main = defineComponent({
day: date.toString()
});
}
if (props.items.length === 0) return;
if (props.items.length === 0)
return;
const renderChildrenImpl = () => props.items.map((item, i) => {
if (!slots || !slots.default) return;
if (!slots || !slots.default)
return;
const el = slots.default({
item
})[0];
if (el.key == null && item.id) el.key = item.id;
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"]}\`
})])]));
}, [
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_) {
@@ -532,17 +576,13 @@ const _sfc_main = defineComponent({
const renderChildren = () => {
const children = renderChildrenImpl();
if (isDebuggerEnabled(6864)) {
const nodes = children.flatMap(node => node ?? []);
const keys = new Set(nodes.map(node => node.key));
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
});
console.warn({ id, debugId: 6864, stack: instances });
}
}
return children;
@@ -555,45 +595,136 @@ const _sfc_main = defineComponent({
el.style.top = "";
el.style.left = "";
}
return () => h(prefer.s.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"
return () => h(
prefer.s.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"
},
...prefer.s.animation ? {
name: "list",
tag: "div",
onBeforeLeave,
onLeaveCanceled
} : {}
},
...prefer.s.animation ? {
name: "list",
tag: "div",
onBeforeLeave,
onLeaveCanceled
} : {}
}, {
default: renderChildren
});
{ 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"
"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};
const MkDateSeparatedList = /* @__PURE__ */ _export_sfc(_sfc_main, [["__cssModules", cssModules]]);
export { MkDateSeparatedList as M };
`.slice(1));
});
it('Composition API (inlined output)', () => {
const code = `
import { a as normalizeClass, b as defineComponent, c as _export_sfc } from './runtime.js';
const CurrentComponent = /* @__PURE__ */ _export_sfc(defineComponent({
__name: "CurrentComponent",
setup() {
return (e, n) => h("div", {
class: normalizeClass([e.$style.root, "extra"])
}, null, 2);
}
}), [["__cssModules", {
"$style": {
root: "x1234"
}
}]]);
export { CurrentComponent as default };
`.slice(1);
const ast = parseAst(code, { sourceType: 'module' });
const magicString = new RolldownMagicString(code);
unwindCssModuleClassName(ast, magicString);
const output = magicString.toString();
expect(output).toContain('class: "x1234 extra"');
expect(output).toContain('defineComponent({');
expect(output).toContain('}), []);');
expect(output).not.toContain('$style');
});
it('should keep cssModules when unresolved references remain', () => {
const code = `
import { a as normalizeClass, b as defineComponent, c as _export_sfc } from './runtime.js';
const CurrentComponent = /* @__PURE__ */ _export_sfc(defineComponent({
__name: "CurrentComponent",
setup() {
return (e, n) => h("div", {
class: normalizeClass([e.$style.root, e.$style[side]])
}, null, 2);
}
}), [["__cssModules", {
"$style": {
root: "x1234"
}
}]]);
export { CurrentComponent as default };
`.slice(1);
const ast = parseAst(code, { sourceType: 'module' });
const magicString = new RolldownMagicString(code);
unwindCssModuleClassName(ast, magicString);
const output = magicString.toString();
expect(output).toContain('e.$style[side]');
expect(output).toContain('__cssModules');
expect(output).not.toContain('}), []);');
});
it('should inline cssModules references used inside class expressions', () => {
const code = `
import { a as classHelper, b as defineComponent, c as _export_sfc } from './runtime.js';
const CurrentComponent = /* @__PURE__ */ _export_sfc(defineComponent({
__name: "CurrentComponent",
setup() {
return (e, n) => h("div", {
class: classHelper([e.$style.root, { [e.$style.main]: isActive }])
}, null, 2);
}
}), [["__cssModules", {
"$style": {
root: "x1234",
main: "x5678"
}
}]]);
export { CurrentComponent as default };
`.slice(1);
const ast = parseAst(code, { sourceType: 'module' });
const magicString = new RolldownMagicString(code);
unwindCssModuleClassName(ast, magicString);
const output = magicString.toString();
expect(output).toContain('class: classHelper(["x1234", { ["x5678"]: isActive }])');
expect(output).toContain('}), []);');
expect(output).not.toContain('$style');
});

View File

@@ -3,17 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { generate } from 'astring';
import { walk } from '../node_modules/estree-walker/src/index.js';
import type * as estree from 'estree';
import type * as estreeWalker from 'estree-walker';
import * as estreeWalker from 'estree-walker';
import type { Plugin } from 'vite';
import type { ESTree } from 'rolldown/utils';
import { RolldownMagicString } from 'rolldown';
function isFalsyIdentifier(identifier: estree.Identifier): boolean {
function isFalsyIdentifier(identifier: Extract<ESTree.Node, { type: 'Identifier' }>): boolean {
return identifier.name === 'undefined' || identifier.name === 'NaN';
}
function normalizeClassWalker(tree: estree.Node, stack: string | undefined): 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') {
@@ -26,7 +25,7 @@ function normalizeClassWalker(tree: estree.Node, stack: string | undefined): str
if (tree.type === 'TemplateLiteral') {
if (tree.expressions.some((x) => x.type !== 'Literal' && (x.type !== 'Identifier' || !isFalsyIdentifier(x)))) return null;
return tree.quasis.reduce((a, c, i) => {
const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial<estree.Literal>).value;
const v = i === tree.quasis.length - 1 ? '' : (tree.expressions[i] as Partial<Extract<ESTree.Node, { type: 'Literal' }>>).value;
return a + c.value.raw + (typeof v === 'string' ? v : '');
}, '');
}
@@ -72,44 +71,144 @@ function normalizeClassWalker(tree: estree.Node, stack: string | undefined): str
tree.type !== 'ChainExpression' &&
tree.type !== 'ConditionalExpression' &&
tree.type !== 'LogicalExpression' &&
tree.type !== 'MemberExpression') {
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, stack?: string): string | null {
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 {
function getPropertyName(node: ESTree.Node, computed: boolean): string | null {
if (node.type === 'Identifier') return computed ? null : node.name;
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
return null;
}
function getMemberPropertyName(node: ESTree.MemberExpression['property'], computed: boolean): string | null {
if (node.type === 'Identifier') return computed ? null : node.name;
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
return null;
}
function findVariableDeclaration(program: ESTree.Program, name: string): ESTree.VariableDeclaration | null {
return program.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;
return x.declarations[0].id.name === name;
}) as ESTree.VariableDeclaration | null;
}
function resolveObjectExpression(program: ESTree.Program, tree: ESTree.Expression): ESTree.ObjectExpression | null {
if (tree.type === 'ObjectExpression') return tree;
if (tree.type !== 'Identifier') return null;
const declaration = findVariableDeclaration(program, tree.name);
if (declaration?.declarations[0].init?.type !== 'ObjectExpression') return null;
return declaration.declarations[0].init;
}
function resolveComponentOptions(program: ESTree.Program, tree: ESTree.Expression): ESTree.ObjectExpression | null {
const target = tree.type === 'Identifier'
? findVariableDeclaration(program, tree.name)?.declarations[0].init ?? null
: tree;
if (target?.type === 'ObjectExpression') return target;
if (target?.type !== 'CallExpression') return null;
if (target.arguments.length !== 1) return null;
if (target.arguments[0].type !== 'ObjectExpression') return null;
return target.arguments[0];
}
function resolveModuleTree(program: ESTree.Program, tree: ESTree.Expression): Map<string, string> | null {
const objectExpression = resolveObjectExpression(program, tree);
if (objectExpression === null) return null;
return new Map(objectExpression.properties.flatMap((property) => {
if (property.type !== 'Property') return [];
const actualKey = getPropertyName(property.key, property.computed);
if (actualKey === null) return [];
if (property.value.type === 'Literal') {
return typeof property.value.value === 'string' ? [[actualKey, property.value.value]] : [];
}
if (property.value.type === 'Identifier') {
const actualValue = findVariableDeclaration(program, property.value.name);
if (actualValue?.declarations[0].init?.type !== 'Literal') return [];
return typeof actualValue.declarations[0].init.value === 'string' ? [[actualKey, actualValue.declarations[0].init.value]] : [];
}
return [];
}));
}
function resolveModuleForest(program: ESTree.Program, tree: ESTree.Expression): Map<string, Map<string, string>> | null {
const objectExpression = resolveObjectExpression(program, tree);
if (objectExpression === null) return null;
return new Map(objectExpression.properties.flatMap((property) => {
if (property.type !== 'Property') return [];
const actualKey = getPropertyName(property.key, property.computed);
if (actualKey === null) return [];
const moduleTree = resolveModuleTree(program, property.value);
return moduleTree === null ? [] : [[actualKey, moduleTree]];
}));
}
function findRenderArrow(options: ESTree.ObjectExpression): Extract<ESTree.Node, { type: 'ArrowFunctionExpression' }> | null {
const setup = options.properties.find((x) => {
if (x.type !== 'Property') return false;
return getPropertyName(x.key, x.computed) === 'setup';
}) as Extract<ESTree.Node, { type: 'Property' }> | undefined;
if (setup?.value.type !== 'FunctionExpression' && setup?.value.type !== 'ArrowFunctionExpression') return null;
if (setup.value.body == null) return null;
if (setup.value.body.type !== 'BlockStatement') return null;
const render = setup.value.body.body.find((x) => x.type === 'ReturnStatement');
if (render?.type !== 'ReturnStatement') return null;
return render.argument?.type === 'ArrowFunctionExpression' ? render.argument : null;
}
function isCssModuleAccess(node: ESTree.Node, ctxName: string, key: string): node is Extract<ESTree.Node, { type: 'MemberExpression' }> {
if (node.type !== 'MemberExpression') return false;
if (node.object.type !== 'MemberExpression') return false;
if (node.object.object.type !== 'Identifier') return false;
if (node.object.object.name !== ctxName) return false;
return getMemberPropertyName(node.object.property, node.object.computed) === key;
}
function isCssModuleReference(node: ESTree.Node, ctxName: string, key: string): node is Extract<ESTree.Node, { type: 'MemberExpression' }> {
if (!isCssModuleAccess(node, ctxName, key)) return false;
return getMemberPropertyName(node.property, node.computed) !== null;
}
function isClassProperty(node: ESTree.Node | null): node is Extract<ESTree.Node, { type: 'Property' }> {
return node?.type === 'Property' && getPropertyName(node.key, node.computed) === 'class';
}
export function unwindCssModuleClassName(ast: ESTree.Node, magicString: RolldownMagicString): void {
(estreeWalker.walk as any)(ast, {
enter(node: ESTree.Node, parent: ESTree.Node | null): void {
//#region
if (parent?.type !== 'Program') return;
if (ast.type !== 'Program') return;
if (node.type !== 'VariableDeclaration') return;
if (node.declarations.length !== 1) return;
if (node.declarations[0].id.type !== 'Identifier') return;
const name = node.declarations[0].id.name;
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;
const componentNode = node.declarations[0].init.arguments[0];
if (componentNode.type !== 'Identifier' && componentNode.type !== 'CallExpression' && componentNode.type !== 'ObjectExpression') return;
if (node.declarations[0].init.arguments[1].type !== 'ArrayExpression') return;
if (node.declarations[0].init.arguments[1].elements.length === 0) return;
const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.findIndex((x) => {
const cssModulesEntry = node.declarations[0].init.arguments[1].elements.find((x) => {
if (x?.type !== 'ArrayExpression') return false;
if (x.elements.length !== 2) return false;
if (x.elements[0]?.type !== 'Literal') return false;
if (x.elements[0].value !== '__cssModules') return false;
if (x.elements[1]?.type !== 'Identifier') return false;
return true;
});
if (!~__cssModulesIndex) return;
}) as ESTree.ArrayExpression | undefined;
const __cssModulesIndex = node.declarations[0].init.arguments[1].elements.indexOf(cssModulesEntry ?? null);
if (cssModulesEntry === undefined || __cssModulesIndex < 0) return;
/* This region assumeed that the entered node looks like the following code.
*
* ```ts
@@ -118,21 +217,10 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#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;
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 cssModuleForest = cssModulesEntry.elements[1];
if (cssModuleForest?.type !== 'Identifier' && cssModuleForest?.type !== 'ObjectExpression') return;
const moduleForest = resolveModuleForest(ast, cssModuleForest);
if (moduleForest === null) return;
/* This region collected a VariableDeclaration node in the module that looks like the following code.
*
* ```ts
@@ -143,35 +231,13 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
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 !== ident) 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;
if (render.argument.params.length !== 2) return;
const ctx = render.argument.params[0];
const options = resolveComponentOptions(ast, componentNode);
if (options === null) return;
const render = findRenderArrow(options);
if (render === null) return;
if (render.params.length !== 2) return;
const ctx = render.params[0];
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
@@ -186,33 +252,8 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
* ```
*/
//#endregion
for (const [key, value] of moduleForest) {
for (const [key, moduleTree] of moduleForest) {
//#region
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 [];
const actualKey = property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? property.key.value : null;
if (typeof actualKey !== 'string') return [];
if (property.value.type === 'Literal') return [[actualKey, property.value.value as string]];
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 [[actualKey, actualValue.declarations[0].init.value as string]];
}));
/* This region collected VariableDeclaration nodes in the module that looks like the following code.
*
* ```ts
@@ -226,17 +267,14 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
(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);
(estreeWalker.walk as any)(render.body, {
enter(childNode: ESTree.Node) {
if (!isCssModuleReference(childNode, ctx.name, key)) return;
const actualKey = getMemberPropertyName(childNode.property, childNode.computed);
if (actualKey === null) return;
const actualValue = moduleTree.get(actualKey);
if (actualValue === undefined) return;
magicString.overwrite(childNode.start, childNode.end, JSON.stringify(actualValue));
this.replace({
type: 'Literal',
value: actualValue,
@@ -276,20 +314,13 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
(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;
console.error(`Undefined style detected: ${key}.${childNode.property.name} (in ${name})`);
this.replace({
type: 'Identifier',
name: 'undefined',
});
(estreeWalker.walk as any)(render.body, {
enter(childNode: ESTree.Node) {
if (!isCssModuleReference(childNode, ctx.name, key)) return;
const actualKey = getMemberPropertyName(childNode.property, childNode.computed);
if (actualKey === null) return;
console.error(`Undefined style detected: ${key}.${actualKey} (in ${name})`);
magicString.overwrite(childNode.start, childNode.end, 'undefined');
},
});
/* This region replaced the reference identifier of missing class names in the render function with `undefined`, as in the following code.
@@ -300,7 +331,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* return openBlock(), createElementBlock('div', {
* class: normalizeClass(_ctx.$style.hoge),
* }, null);
* };
@@ -316,7 +347,7 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
* ...
* return (_ctx, _cache) => {
* ...
* return openBlock(), createElementBlock("div", {
* return openBlock(), createElementBlock('div', {
* class: normalizeClass(undefined),
* }, null);
* };
@@ -326,18 +357,15 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#endregion
//#region
(walk as typeof estreeWalker.walk)(render.argument.body, {
enter(childNode) {
(estreeWalker.walk as any)(render.body, {
enter(childNode: ESTree.Node, childParent: ESTree.Node | null) {
if (childNode.type !== 'CallExpression') return;
if (childNode.callee.type !== 'Identifier') return;
if (childNode.callee.name !== 'normalizeClass') return;
if (childNode.arguments.length !== 1) return;
if (childNode.callee.type === 'Identifier' && childNode.callee.name !== 'normalizeClass' && !isClassProperty(childParent)) return;
if (childNode.callee.type !== 'Identifier' && !isClassProperty(childParent)) return;
const normalized = normalizeClass(childNode.arguments[0], name);
if (normalized === null) return;
this.replace({
type: 'Literal',
value: normalized,
});
magicString.overwrite(childNode.start, childNode.end, JSON.stringify(normalized));
},
});
/* This region compiled the `normalizeClass` call into a pseudo-AOT compilation, as in the following code.
@@ -374,19 +402,34 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
*/
//#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,
});
const hasRemainingCssModuleReference = Array.from(moduleForest.keys()).some((key) => {
let found = false;
(estreeWalker.walk as any)(render.body, {
enter(childNode: ESTree.Node) {
if (!isCssModuleAccess(childNode, ctx.name, key)) return;
found = true;
this.skip();
},
});
this.remove();
return found;
});
if (hasRemainingCssModuleReference) return;
//#region
if (node.declarations[0].init.arguments[1].elements.length === 1) {
if (componentNode.type === 'Identifier') {
(estreeWalker.walk as any)(ast, {
enter(childNode: ESTree.Node) {
if (childNode.type !== 'Identifier') return;
if (childNode.name !== componentNode.name) return;
magicString.overwrite(childNode.start, childNode.end, name);
},
});
magicString.remove(node.start, node.end);
} else {
const removeStart = cssModulesEntry.start;
const removeEnd = node.declarations[0].init.arguments[1].end - 1;
magicString.remove(removeStart, removeEnd);
}
/* NOTE: The above logic is valid as long as the following two conditions are met.
*
* - the uniqueness of `ident` is kept throughout the module
@@ -411,31 +454,10 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
});
*/
} else {
this.replace({
type: 'VariableDeclaration',
declarations: [{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: node.declarations[0].id.name,
},
init: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: '_export_sfc',
},
arguments: [{
type: 'Identifier',
name: ident,
}, {
type: 'ArrayExpression',
elements: node.declarations[0].init.arguments[1].elements.slice(0, __cssModulesIndex).concat(node.declarations[0].init.arguments[1].elements.slice(__cssModulesIndex + 1)),
}],
},
}],
kind: 'const',
});
const nextElement = node.declarations[0].init.arguments[1].elements[__cssModulesIndex + 1];
const removeStart = node.declarations[0].init.arguments[1].elements[__cssModulesIndex]!.start;
const removeEnd = nextElement ? nextElement.start : node.declarations[0].init.arguments[1].end - 1;
magicString.remove(removeStart, removeEnd);
}
/* This region removed the `__cssModules` reference from the second argument of `_export_sfc`, as in the following code.
*
@@ -474,10 +496,11 @@ export function unwindCssModuleClassName(ast: estree.Node): void {
export default function pluginUnwindCssModuleClassName(): Plugin {
return {
name: 'UnwindCssModuleClassName',
renderChunk(code): { code: string } {
const ast = this.parse(code) as unknown as estree.Node;
unwindCssModuleClassName(ast);
return { code: generate(ast) };
renderChunk(code, _chunk, _options, meta) {
const ast = ('ast' in meta ? meta.ast ?? this.parse(code) : this.parse(code)) as ESTree.Program;
const magicString = meta.magicString ?? new RolldownMagicString(code);
unwindCssModuleClassName(ast, magicString);
return magicString;
},
};
}

View File

@@ -13,11 +13,12 @@ import {
type LogOptions,
normalizePath,
type Plugin,
type PluginOption
type PluginOption,
} from 'vite';
import fs from 'node:fs';
import JSON5 from 'json5';
import MagicString, { SourceMap } from 'magic-string';
import { RolldownMagicString } from 'rolldown';
import type { TransformResult } from 'rolldown';
import path from 'node:path'
import { hash, toBase62 } from '../vite.config';
import { minimatch } from 'minimatch';
@@ -63,7 +64,7 @@ interface MarkerRelation {
let logger = {
info: (msg: string, options?: LogOptions) => { },
warn: (msg: string, options?: LogOptions) => { },
error: (msg: string, options?: LogErrorOptions | unknown) => { },
error: (msg: string, options?: LogErrorOptions) => { },
};
let loggerInitialized = false;
@@ -460,9 +461,18 @@ function propertyAccessProxy(path: string[]): AccessProxy {
const i18nProxy = propertyAccessProxy(['i18n']);
export function collectFileMarkers(id: string, code: string): SearchIndexItem[] {
export function collectFileMarkers(id: string, code: string | RolldownMagicString | undefined): SearchIndexItem[] {
try {
const { descriptor, errors } = vueSfcParse(code, {
let codeStr: string;
if (typeof code === 'string') {
codeStr = code;
} else if (code != null) {
codeStr = code.toString();
} else {
throw new Error(`Code is undefined for file ${id}`);
}
const { descriptor, errors } = vueSfcParse(codeStr, {
filename: id,
});
@@ -473,7 +483,8 @@ export function collectFileMarkers(id: string, code: string): SearchIndexItem[]
return extractUsageInfoFromTemplateAst(descriptor.template?.ast, id);
} catch (error) {
logger.error(`Error analyzing file ${id}:`, error);
let _error = error instanceof Error ? error : new Error(String(error));
logger.error(`Error analyzing file ${id}:`, { error: _error });
}
return [];
@@ -481,10 +492,7 @@ export function collectFileMarkers(id: string, code: string): SearchIndexItem[]
// endregion
type TransformedCode = {
code: string,
map: SourceMap,
};
type TransformedCode = Exclude<TransformResult, string>;
export class MarkerIdAssigner {
// key: file id
@@ -509,13 +517,12 @@ export class MarkerIdAssigner {
}
#processImpl(id: string, code: string): TransformedCode {
const s = new MagicString(code); // magic-string のインスタンスを作成
const s = new RolldownMagicString(code); // magic-string のインスタンスを作成
const parsed = vueSfcParse(code, { filename: id });
if (!parsed.descriptor.template) {
return {
code,
map: s.generateMap({ source: id, includeContent: true }),
code, // テンプレートがない場合は元のコードを返す
};
}
const ast = parsed.descriptor.template.ast; // テンプレート AST を取得
@@ -523,8 +530,7 @@ export class MarkerIdAssigner {
if (!ast) {
return {
code: s.toString(), // 変更後のコードを返す
map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
code,
};
}
@@ -611,7 +617,6 @@ export class MarkerIdAssigner {
return {
code: s.toString(), // 変更後のコードを返す
map: s.generateMap({ source: id, includeContent: true }), // ソースマップも生成 (sourceMap: true が必要)
};
}
@@ -642,7 +647,7 @@ export class MarkerIdAssigner {
}
}
// Rollup プラグインとして export
// Vite プラグインとして export
export default function pluginCreateSearchIndex(options: Options): PluginOption {
const assigner = new MarkerIdAssigner();
return [

View File

@@ -1,7 +1,10 @@
// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import JSON5 from 'json5';
import { Plugin } from 'rollup';
import { Plugin } from 'vite';
import { createFilter, dataToEsm } from '@rollup/pluginutils';
import { RollupJsonOptions } from '@rollup/plugin-json';

View File

@@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import path from 'node:path'
import path from 'node:path';
import locales from 'i18n';
import type { Plugin } from 'vite';
const localesDir = path.resolve(__dirname, '../../../locales')
/**
* 外部ファイルを監視し、必要に応じてwebSocketでメッセージを送るViteプラグイン
* @returns {import('vite').Plugin}
*/
export default function pluginWatchLocales() {
export default function pluginWatchLocales(): Plugin {
return {
name: 'watch-locales',

View File

@@ -21,10 +21,7 @@
"@github/webauthn-json": "2.1.1",
"@mcaptcha/core-glue": "0.1.0-alpha-5",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.45.0",
"@sentry/vue": "10.40.0",
"@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
@@ -64,21 +61,20 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.60.0",
"sanitize-html": "2.17.2",
"sass": "1.98.0",
"shiki": "3.23.0",
"textarea-caret": "3.1.0",
"three": "0.183.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"v-code-diff": "1.13.1",
"vite": "7.3.1",
"vue": "3.5.30",
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.5",
"@rollup/plugin-json": "6.1.0",
"@rollup/pluginutils": "5.3.0",
"@storybook/addon-essentials": "8.6.18",
"@storybook/addon-interactions": "8.6.18",
"@storybook/addon-links": "10.3.3",
@@ -112,7 +108,7 @@
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/coverage-v8": "4.1.1",
"@vitest/coverage-v8": "4.1.2",
"@vue/compiler-core": "3.5.30",
"acorn": "8.16.0",
"astring": "1.9.0",
@@ -123,7 +119,6 @@
"estree-walker": "3.0.3",
"happy-dom": "20.8.8",
"intersection-observer": "0.12.2",
"magic-string": "0.30.21",
"micromatch": "4.0.8",
"minimatch": "10.2.4",
"msw": "2.12.14",
@@ -132,14 +127,17 @@
"prettier": "3.8.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"rolldown": "1.0.0-rc.13",
"sass-embedded": "1.98.0",
"seedrandom": "3.0.5",
"start-server-and-test": "2.1.5",
"storybook": "10.3.3",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.21.0",
"vite": "8.0.7",
"vite-plugin-glsl": "1.5.6",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "4.1.1",
"vitest": "4.1.2",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.2.6",
"vue-eslint-parser": "10.4.0",

View File

@@ -5,9 +5,9 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { markRaw, ref, defineAsyncComponent, nextTick, effectScope, isRef, shallowReactive, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { Component, MaybeRef } from 'vue';
import type { Component, MaybeRef, ShallowReactive } from 'vue';
import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
@@ -145,7 +145,7 @@ let popupIdCount = 0;
export const popups = ref<{
id: number;
component: Component;
props: Record<string, any>;
props: ShallowReactive<Record<string, any>>;
events: Record<string, any>;
}[]>([]);
@@ -183,6 +183,32 @@ type ComponentEmitsObject<C extends Component, IE = OverloadToUnion<ComponentEmi
: (...args: any[]) => void;
}>;
// ref をそのまま保持せず popup 側の reactive props に同期するようにして、スコープをまたいでリアクティビティが切れるのを防止する
function normalizePopupProps<T extends Record<string, any>>(props: T): {
resolvedProps: ShallowReactive<T>;
stopSync: () => void;
} {
const resolvedProps = shallowReactive<T>({} as T) as T; // shallowReactiveの返り値はreadonlyだが、実際には書き換えるので元の型で扱う
const scope = effectScope();
scope.run(() => {
for (const [key, value] of Object.entries(props)) {
if (isRef(value)) {
watch(value, (resolvedValue) => {
resolvedProps[key as keyof T] = resolvedValue as T[keyof T];
}, { immediate: true });
} else {
resolvedProps[key as keyof T] = value;
}
}
});
return {
resolvedProps: resolvedProps as ShallowReactive<T>,
stopSync: () => scope.stop(),
};
}
// NOTE: ジェネリック型つきのコンポーネントでは、emitsの型推論がうまく働かない型変数を取り出すことはできないため
// NOTE: emitsがOverloadToUnionで対応しているオーバーロードの数を超える場合は、OverloadToUnionの個数を増やせばOK
export function popup<T extends Component>(
@@ -193,20 +219,24 @@ export function popup<T extends Component>(
markRaw(component);
const id = ++popupIdCount;
const { resolvedProps, stopSync } = normalizePopupProps(props);
let disposed = false;
const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => {
if (disposed) return;
disposed = true;
stopSync();
nextTick(() => {
popups.value = popups.value.filter(p => p.id !== id);
}, 0);
};
const state = {
component,
props,
events,
id,
});
};
popups.value.push(state);
popups.value.push({
component,
props: resolvedProps,
events,
id,
});
return {
dispose,
@@ -241,27 +271,7 @@ export async function popupAsyncWithDialog<T extends Component>(
window.clearTimeout(timer);
closeWaiting();
markRaw(component);
const id = ++popupIdCount;
const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => {
popups.value = popups.value.filter(p => p.id !== id);
}, 0);
};
const state = {
component,
props,
events,
id,
};
popups.value.push(state);
return {
dispose,
};
return popup(component, props, events);
}
export function pageWindow(path: string) {

View File

@@ -1,7 +1,7 @@
import path from 'path';
import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
import pluginGlsl from 'vite-plugin-glsl';
import { replacePlugin } from 'rolldown/plugins';
import type { UserConfig } from 'vite';
import { defineConfig } from 'vite';
import * as yaml from 'js-yaml';
@@ -11,13 +11,13 @@ import locales from 'i18n';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
import pluginJson5 from './lib/vite-plugin-json5.js';
import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
import pluginWatchLocales from './lib/vite-plugin-watch-locales.js';
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n.js';
const url = process.env.NODE_ENV === 'development' ? yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')).url : null;
const url = process.env.NODE_ENV === 'development' ? (yaml.load(await fsp.readFile('../../.config/default.yml', 'utf-8')) as any).url : null;
const host = url ? (new URL(url)).hostname : undefined;
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
@@ -121,11 +121,10 @@ export function getConfig(): UserConfig {
pluginGlsl({ minify: true }),
...process.env.NODE_ENV === 'production'
? [
pluginReplace({
replacePlugin({
'isChromatic()': JSON.stringify(false),
}, {
preventAssignment: true,
values: {
'isChromatic()': JSON.stringify(false),
},
}),
]
: [],
@@ -154,11 +153,6 @@ export function getConfig(): UserConfig {
}
},
},
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
},
define: {
@@ -178,7 +172,10 @@ export function getConfig(): UserConfig {
'safari16',
],
manifest: 'manifest.json',
rollupOptions: {
rolldownOptions: {
experimental: {
nativeMagicString: true,
},
input: {
i18n: './src/i18n.ts',
entry: './src/_boot_.ts',
@@ -186,11 +183,18 @@ export function getConfig(): UserConfig {
external: externalPackages.map(p => p.match),
preserveEntrySignatures: 'allow-extension',
output: {
manualChunks: {
vue: ['vue'],
photoswipe: ['photoswipe', 'photoswipe/lightbox', 'photoswipe/style.css'],
// dependencies of i18n.ts
'config': ['@@/js/config.js'],
codeSplitting: {
groups: [{
name: 'vue',
test: /node_modules[\\/]vue/,
}, {
name: 'photoswipe',
test: /node_modules[\\/]photoswipe/,
}, {
// dependencies of i18n.ts
name: 'config',
test: /@@[\\/]js[\\/]config\.js/,
}],
},
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,

View File

@@ -41,13 +41,13 @@
"@types/node": "24.12.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitest/coverage-v8": "4.1.1",
"@vitest/coverage-v8": "4.1.2",
"esbuild": "0.27.4",
"execa": "9.6.1",
"ncp": "2.0.0",
"nodemon": "3.1.14",
"tsd": "0.33.0",
"vitest": "4.1.1",
"vitest": "4.1.2",
"vitest-websocket-mock": "0.5.0"
},
"files": [