|
| 1 | +// adapted from https://github.com/nuxt/image/blob/5415ba721e8cb1ec15205f9bf54ada2e3d5fe07d/test/unit/bundle.test.ts |
| 2 | +import process from "node:process"; |
| 3 | +import { join } from "node:path"; |
| 4 | +import { fileURLToPath } from "node:url"; |
| 5 | +import { mkdir, writeFile, rm, lstat, readFile } from "node:fs/promises"; |
| 6 | +import { buildNuxt, loadNuxt } from "@nuxt/kit"; |
| 7 | +import type { NuxtConfig } from "@nuxt/schema"; |
| 8 | +import { describe, it, expect } from "vitest"; |
| 9 | +import { glob } from "tinyglobby"; |
| 10 | +import { isWindows } from "std-env"; |
| 11 | +import { defu } from "defu"; |
| 12 | + |
| 13 | +describe.skipIf(process.env.ECOSYSTEM_CI || isWindows)( |
| 14 | + "nuxt i18n bundle size", |
| 15 | + () => { |
| 16 | + it("should match snapshot", { timeout: 120_000 }, async () => { |
| 17 | + const rootDir = fileURLToPath(new URL("../.tmp", import.meta.url)); |
| 18 | + |
| 19 | + await rm(rootDir, { recursive: true, force: true }).catch(() => null); |
| 20 | + |
| 21 | + const [base, withModule, withVueI18n, withVueI18nDropCompiler] = |
| 22 | + await Promise.all([ |
| 23 | + build(join(rootDir, "without")), |
| 24 | + build(join(rootDir, "with"), { |
| 25 | + modules: ["@nuxtjs/i18n"], |
| 26 | + // i18n: { |
| 27 | + // defaultLocale: "en", |
| 28 | + // locales: [ |
| 29 | + // { code: "en", file: "en.json" }, |
| 30 | + // { code: "fr", file: "fr.json" }, |
| 31 | + // ], |
| 32 | + // }, |
| 33 | + }), |
| 34 | + build(join(rootDir, "vue-i18n"), {}, { vueI18n: true }), |
| 35 | + build( |
| 36 | + join(rootDir, "vue-i18n-drop-compiler"), |
| 37 | + { |
| 38 | + vite: { |
| 39 | + define: { |
| 40 | + __INTLIFY_JIT_COMPILATION__: true, |
| 41 | + __INTLIFY_DROP_MESSAGE_COMPILER__: true, |
| 42 | + }, |
| 43 | + }, |
| 44 | + }, |
| 45 | + { vueI18n: true }, |
| 46 | + ), |
| 47 | + ]); |
| 48 | + |
| 49 | + const data = { |
| 50 | + // total bundle size increase |
| 51 | + module: roundToKilobytes(withModule.totalBytes - base.totalBytes), |
| 52 | + "module (without vue-i18n)": roundToKilobytes(withModule.totalBytes - withVueI18n.totalBytes), |
| 53 | + "vue-i18n": roundToKilobytes(withVueI18n.totalBytes - base.totalBytes), |
| 54 | + "vue-i18n (without message compiler)": roundToKilobytes(withVueI18nDropCompiler.totalBytes - base.totalBytes), |
| 55 | + }; |
| 56 | + |
| 57 | + expect(data).toMatchInlineSnapshot(` |
| 58 | + { |
| 59 | + "module": "69.4k", |
| 60 | + "module (without vue-i18n)": "26.0k", |
| 61 | + "vue-i18n": "43.3k", |
| 62 | + "vue-i18n (without message compiler)": "26.8k", |
| 63 | + } |
| 64 | + `); |
| 65 | + }); |
| 66 | + }, |
| 67 | +); |
| 68 | + |
| 69 | +async function build( |
| 70 | + rootDir: string, |
| 71 | + config: NuxtConfig = {}, |
| 72 | + options: { vueI18n?: boolean } = {}, |
| 73 | +) { |
| 74 | + await mkdir(rootDir, { recursive: true }); |
| 75 | + |
| 76 | + const tree: Record<string, string> = { |
| 77 | + // "/i18n/locales/en.json": `{ "hello": "Hello" }`, |
| 78 | + // "/i18n/locales/fr.json": `{ "hello": "Bonjour" }`, |
| 79 | + // "/pages/index.vue": `<template>{{ $t('hello') }}</template>`, |
| 80 | + "/app.vue": `<template><NuxtPage /></template>`, |
| 81 | + }; |
| 82 | + |
| 83 | + if (options.vueI18n) { |
| 84 | + const template = [ |
| 85 | + `<script setup lang="ts">`, |
| 86 | + `import { useI18n } from 'vue-i18n'`, |
| 87 | + `const { t } = useI18n()`, |
| 88 | + `</script>`, |
| 89 | + `<template><div>{{ t('hello') }}</div></template>`, |
| 90 | + ].join("\n"); |
| 91 | + |
| 92 | + if (tree["/pages/index.vue"]) { |
| 93 | + tree["/pages/index.vue"] = template; |
| 94 | + } else { |
| 95 | + tree["/app.vue"] = template; |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + for (const [path, content] of Object.entries(tree)) { |
| 100 | + const fullPath = join(rootDir, path); |
| 101 | + const dir = join(fullPath, ".."); |
| 102 | + if (dir !== rootDir) { |
| 103 | + await mkdir(join(fullPath, ".."), { recursive: true }); |
| 104 | + } |
| 105 | + await writeFile(fullPath, content); |
| 106 | + } |
| 107 | + |
| 108 | + const nuxt = await loadNuxt({ |
| 109 | + cwd: rootDir, |
| 110 | + ready: true, |
| 111 | + overrides: { |
| 112 | + // ssr: false, |
| 113 | + ...defu(config, { |
| 114 | + vite: { |
| 115 | + define: { |
| 116 | + __INTLIFY_PROD_DEVTOOLS__: false, // for vue-i18n build - disabled in nuxt-i18n bundler.ts |
| 117 | + }, |
| 118 | + // to disable minification for easier size analysis |
| 119 | + // $client: { |
| 120 | + // build: { |
| 121 | + // minify: false, |
| 122 | + // rollupOptions: { |
| 123 | + // output: { |
| 124 | + // chunkFileNames: "_nuxt/[name].js", |
| 125 | + // entryFileNames: "_nuxt/[name].js", |
| 126 | + // }, |
| 127 | + // }, |
| 128 | + // }, |
| 129 | + // }, |
| 130 | + }, |
| 131 | + }), |
| 132 | + }, |
| 133 | + }); |
| 134 | + await buildNuxt(nuxt); |
| 135 | + await nuxt.close(); |
| 136 | + return await analyzeSizes(["**/*.js"], join(rootDir, ".output/public")); |
| 137 | +} |
| 138 | + |
| 139 | +async function analyzeSizes(pattern: string[], rootDir: string) { |
| 140 | + const files: string[] = await glob(pattern, { cwd: rootDir }); |
| 141 | + let totalBytes = 0; |
| 142 | + for (const file of files) { |
| 143 | + const path = join(rootDir, file); |
| 144 | + const isSymlink = (await lstat(path).catch(() => null))?.isSymbolicLink(); |
| 145 | + |
| 146 | + if (!isSymlink) { |
| 147 | + const bytes = Buffer.byteLength(await readFile(path)); |
| 148 | + totalBytes += bytes; |
| 149 | + } |
| 150 | + } |
| 151 | + return { files, totalBytes }; |
| 152 | +} |
| 153 | + |
| 154 | +function roundToKilobytes(bytes: number) { |
| 155 | + return (bytes / 1024).toFixed(bytes > 100 * 1024 ? 0 : 1) + "k"; |
| 156 | +} |
0 commit comments