lisonge / vite-plugin-monkey

A vite plugin server and build your.user.js for userscript engine like Tampermonkey, Violentmonkey, Greasemonkey, ScriptCat
MIT License
1.33k stars 70 forks source link

main.ts 中使用 await 关键字,build失败 #25

Closed marioplus closed 1 year ago

marioplus commented 1 year ago

感谢抽空查看我问题,我是前端新手,所以对问题的描述可能有点混乱,请见谅。

问题描述

注意:问题出现在使用build命令时,dev指令不会出现该错误

项目是以纯ts模式开发的,运行vite build出现以下错误:

PS G:\Code\tampermokey\steam-price-exchanger> vite build
vite v3.1.4 building for production...
✓ 14 modules transformed.
Module format iife does not support top-level await. Use the "es" or "system" output formats rather.
file: G:\Code\tampermokey\steam-price-exchanger\src\main.ts
error during build:
Error: Module format iife does not support top-level await. Use the "es" or "system" output formats rather.
    at error (file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/rollup/dist/es/shared/rollup.js:1858:30)
    at Chunk.render (file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/rollup/dist/es/shared/rollup.js:14844:20)
    at file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/rollup/dist/es/shared/rollup.js:16006:52
    at Array.map (<anonymous>)
    at Bundle.addFinalizedChunksToBundle (file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/rollup/dist/es/shared/rollup.js:16004:34)
    at Bundle.generate (file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/rollup/dist/es/shared/rollup.js:15984:24)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/rollup/dist/es/shared/rollup.js:23754:27
    at async catchUnfinishedHookActions (file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/rollup/dist/es/shared/rollup.js:23088:20)
    at async doBuild (file:///G:/Code/tampermokey/steam-price-exchanger/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:45818:26)

如何重现

可在此处查看项目代码

main.ts中使用await关键字,执行vite build指令失败,例如:

import {main} from './realMain'

await main()

main.ts中不使用await关键字,执行vite build指令成功,例如:

import {main} from './realMain'

main().then()

项目配置

可以在[此处]()获取项目的源码

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "target": "ESNext",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "lib": [
            "ESNext",
            "DOM"
        ],
        "moduleResolution": "Node",
        "strict": true,
        "sourceMap": true,
        "resolveJsonModule": true,
        "isolatedModules": true,
        "esModuleInterop": true,
        "noEmit": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noImplicitReturns": true,
        "skipLibCheck": true
    },
    "include": [
        "src"
    ]
}
import {defineConfig} from 'vite'
import monkey from 'vite-plugin-monkey'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        monkey({
            entry: 'src/main.ts',
            userscript: {
                name: 'steam价格转换',
                author: 'marioplus',
                description: 'steam商店中的价格转换为人民币',
                version: '1.0.0',
                icon: 'https://vitejs.dev/logo.svg',
                namespace: 'marioplus/steam-price-exchanger',
                match: [
                    'https://store.steampowered.com/*',
                    'https://steamcommunity.com/*'
                ],
                connect: [
                    'open.er-api.com',
                    'store.steampowered.com'
                ]
            },
        }),
    ],
})

尝试解决方案

将修改vite配置增加 build 配置

export default defineConfig({
    build: {
        lib: {
            entry: 'src/main.ts',
            formats: ['es'],
        },
        target: 'esnext'
    }
    ...
})

会出现以下错误

"C:\Program Files\nodejs\npm.cmd" run build

> steam-price-converter@1.0.0 build
> tsc && vite build

vite v3.1.4 building for production...
✓ 15 modules transformed.
dist/steam-price-converter.user.js   90.44 KiB / gzip: 18.08 KiB
Module format iife does not support top-level await. Use the "es" or "system" output formats rather.
file: G:\Code\tampermokey\steam-price-converter\src\main.ts
error during build:
Error: Module format iife does not support top-level await. Use the "es" or "system" output formats rather.
    at error (file:///G:/Code/tampermokey/steam-price-converter/node_modules/rollup/dist/es/shared/rollup.js:1858:30)
    at Chunk.render (file:///G:/Code/tampermokey/steam-price-converter/node_modules/rollup/dist/es/shared/rollup.js:14844:20)
    at file:///G:/Code/tampermokey/steam-price-converter/node_modules/rollup/dist/es/shared/rollup.js:16006:52
    at Array.map (<anonymous>)
    at Bundle.addFinalizedChunksToBundle (file:///G:/Code/tampermokey/steam-price-converter/node_modules/rollup/dist/es/shared/rollup.js:16004:34)
    at Bundle.generate (file:///G:/Code/tampermokey/steam-price-converter/node_modules/rollup/dist/es/shared/rollup.js:15984:24)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async file:///G:/Code/tampermokey/steam-price-converter/node_modules/rollup/dist/es/shared/rollup.js:23754:27
    at async catchUnfinishedHookActions (file:///G:/Code/tampermokey/steam-price-converter/node_modules/rollup/dist/es/shared/rollup.js:23088:20)
    at async doBuild (file:///G:/Code/tampermokey/steam-price-converter/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:45818:26)

虽然出现了错误,但dist文件夹有构建脚本输出,且脚本能正常使用

lisonge commented 1 year ago

vite 模块的打包是基于 rollup,目前 rollup 并不支持 iife top await 一个可行的解决方法是把异步代码包裹在 async 里

import {xxx} from './xxx'
(async () => {
  console.log(await fetch('/'));
})();

就像仓库示例里的一样 https://github.com/lisonge/vite-plugin-monkey/blob/7d8d38696e7111869c8015da22646e102d504245/playground/dynamic-import/src/main.ts#L10-L16


另一种可行的方法是使用自定义 plugin 在 build 模式下修改入口代码

// src/main.ts
import { x } from './util';
console.log(x);
// top-await-wrap-start
console.log(await fetch('/'));
// vite.config.ts
import { defineConfig } from 'vite';
import monkey, { cdn } from 'vite-plugin-monkey';

export default defineConfig(async ({ command, mode }) => ({
  plugins: [
    monkey({
      entry: 'src/main.ts',
      userscript: {
        namespace: 'https://github.com/lisonge',
        icon: 'https://vitejs.dev/logo.svg',
        match: 'https://i.songe.li/*',
        description: 'default_description',
      },
    }),
    {
      name: 'entry-top-await',
      apply: 'build',
      enforce: 'pre',
      transform(code, id) {
        if (
          id.endsWith('src/main.ts') &&
          code.includes(`// top-await-wrap-start`)
        ) {
          return (
            code.replace('// top-await-wrap-start', '(async()=>{\n') + '\n})();'
          );
        }
      },
    },
  ],
}));
lisonge commented 1 year ago

尝试解决方案 后仍然报错是因为这时候 vite 在同时使用 esm 和 iife 构建,esm 没有报错正常输出,是之前的 iife 在报错

如你所见 vite 必须以 target: 'esnext' 构建才能使用 top-await ,这意味着不做任何语法转译,在非最新浏览器存在兼容性问题 但是代码在 tampermonkey 中实际上是被包裹在 async function 中运行,并不是直接运行单个文件

window['__p__9012200.7534899'] = function () {
    ((context, powers, fapply) => {
        with (context) {
            ((module) => {
                'use strict';
                try {
                    fapply(module, context, [
                        undefined,
                        undefined,
                        powers.CDATA,
                        powers.uneval,
                        powers.define,
                        powers.module,
                        powers.exports,
                        powers.GM,
                        powers.GM_info,
                    ]);
                } catch (e) {
                    if (e.message && e.stack) {
                        console.error(
                            "ERROR: Execution of script 'New Userscript' failed! " +
                                e.message
                        );
                        console.log(e.stack);
                    } else {
                        console.error(e);
                    }
                }
            })(async function (
                context,
                fapply,
                CDATA,
                uneval,
                define,
                module,
                exports,
                GM,
                GM_info
            ) {
                // ==UserScript==
                // @name         New Userscript
                // @namespace    http://tampermonkey.net/
                // @version      0.1
                // @description  try to take over the world!
                // @author       You
                // @match        https://i.songe.li/
                // @icon         https://www.google.com/s2/favicons?sz=64&domain=songe.li
                // @grant        none
                // ==/UserScript==

                (function () {
                    'use strict';

                    // Your code here...
                })();
                console.log(await 1);
            });
        }
    })(this.context, this.powers, this.fapply);
    //# sourceURL=chrome-extension://iikmkjmpaadaobahmlepeloendndfphd/userscript.html?name=New%2520Userscript.user.js&id=78aba2bf-2407-41ab-8072-ca1bbbbb5f6f
};

综合下来的我比较推荐的解决方案是将你的主代码用 (async()=>{})(); 包裹运行

marioplus commented 1 year ago

原来是这样,再次感谢大佬解惑,祝您生活愉快。

lisonge commented 1 year ago

@marioplus 一些小小的建议

vite 是支持 import json 的,这意味着你的 src/County.ts 可以改成

export type County = {
    code: string;
    name: string;
    nameEn: string;
    currencyCode: string;
};
import countyCurrencyCodes from './countyCurrencyCodes.json';
export const counties = new Map<string, County>(
    Object.entries(countyCurrencyCodes).map(([k, v]) => [
        k,
        {
            ...v,
            currencyCode: v.currency,
        },
    ])
);

另外你的 src/realMain.ts#L8-L31 里的 css 也可以提取出来成为单个 css 文件,然后改成 import css 的格式 image 好处是可以利用 vite 的模块热替换,dev 模式下只需要更改这个 css 文件而无需刷新页面即可看到样式效果,在build模式下这块的css还会被压缩,可以减少构建产物

/*src/style.css*/
    .tab_item_discount {
      min-width: 113px !important;
      width: unset;
    }
    .discount_final_price {
      display: inline-block !important;
    }

    /*商店搜索列表*/
    .search_result_row
    .col.search_price {
      width: 175px;
    }
    .search_result_row
    .col.search_name {
      width: 200px;
    }

    /*市场列表*/
    .market_listing_their_price {
      width: 160px;
    }
// src/realMain.ts
import {GM_cookie, GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client'
import {ConverterManager} from './converter/ConverterManager'
import {counties} from './County'
import {ExchangeRateManager} from './remote/ExchangeRateManager'
import './style.css'

export async function main() {
    // 获取国家代码
    let countyCode: string = await getCountyCode()
    if (!countyCode || countyCode.length === 0) {
        throw Error('获取国家代码失败!')
    }

    console.log('countyCode', countyCode)

    if (countyCode === 'CN') {
        console.log('人名币无需转换')
    } else {
        await convert(countyCode)
    }
}

另外你的 reflect-metadata 也可以使用 externalGlobals 使用 cdn 加载,这可以让你的构建产物大大减少

import {defineConfig} from 'vite'
import monkey,{cdn} from 'vite-plugin-monkey'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        monkey({
            entry: 'src/main.ts',
            userscript: {
                name: 'steam价格转换',
                author: 'marioplus',
                description: 'steam商店中的价格转换为人民币',
                version: '1.0.3',
                icon: 'https://vitejs.dev/logo.svg',
                namespace: 'https://github.com/marioplus/steam-price-converter',
                license: 'AGPL-3.0-or-later',
                match: [
                    'https://store.steampowered.com/*',
                    'https://steamcommunity.com/*'
                ],
                connect: [
                    'open.er-api.com',
                    'store.steampowered.com'
                ]
            },
            build:{
                externalGlobals:{
                    'reflect-metadata':cdn.jsdelivr('', 'Reflect.js')
                }
            }
        }),
    ],
})

以上操作可以让你的构建产物从 70.46 KiB 减少到 46.23 KiB image


另外如果你觉得 vite-plugin-monkey/dist/client 名字太长,可以使用 $ 代替 也就是 import { GM_cookie, GM_xmlhttpRequest } from 'vite-plugin-monkey/dist/client'; 可以改成 import { GM_cookie, GM_xmlhttpRequest } from '$';

marioplus commented 1 year ago

感谢百忙中抽时间指点,上面的几点建议我觉得十分有用。不过其中关于第一点建议我有点疑问,通过import json的方式导入数据相对我之前直接使用Map定义的优势在哪里?是json格式数据更方便的应用在其他的地方,且后期需要对数据维护更方便吗?

另外有一个不情之请,如果可以的话,可以帮忙做下项目的 code review 吗?即使您不愿意也很正常。再次感谢指点,祝您生活愉快。

lisonge commented 1 year ago

感谢百忙中抽时间指点

哈哈,百忙 不至于,只是国庆家里蹲7天太无聊了


不好意思 json 那块代码写错了,应该是

import countyCurrencyCodes from './countyCurrencyCodes.json';
export const counties = new Map<string, County>(
    countyCurrencyCodes.map((v) => [
        v.code,
        {
            ...v,
            currencyCode: v.currency,
        },
    ])
);

通过import json的方式导入数据相对我之前直接使用Map定义的优势在哪里?

只是个人习惯,我习惯 单一功能原则 减少重复代码

因为你的 src/County.tssrc/countyCurrencyCodes.json 有很高的重复度

后续修改只需要改一处而不是多处


可以帮忙做下项目的 code review 吗?

感觉其他地方也没必要修改了,更多的是个人习惯问题

比如我会把 src/remote/Http.ts 里的 import {XhrRequest} from 'vite-plugin-monkey/src/client/types' 修改成 import type { XhrRequest } from '$';

lisonge commented 1 year ago

reflect-metadata 的 cdn 使用 bug 说明

https://github.com/marioplus/steam-price-converter/commit/afd0ff6320820b84a498b4b7b59efc806519ae18#commitcomment-86011177

marioplus commented 1 year ago

感谢大佬,真是长见识了,@require 还能这样写,这样解决这个问题,真是绝了。

lisonge commented 1 year ago

补充一下, top level await 自 v3.0.0 开始支持,当前版本是 v.3.4.1