diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c66ccc6c9..1a1e20956 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,25 @@ jobs: ci: uses: harlan-zw/nuxt-seo/.github/workflows/reusable-ci.yml@main + packed-install-smoke: + name: Packed install smoke + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: lts/* + + - uses: pnpm/action-setup@v6 + with: + package_json_file: package.json + + - run: pnpm install --frozen-lockfile + + - run: pnpm run test:packed-install + takumi-e2e-matrix: name: Takumi e2e matrix runs-on: ubuntu-latest diff --git a/package.json b/package.json index bf539eeb2..d5875c5e2 100644 --- a/package.json +++ b/package.json @@ -62,9 +62,10 @@ "prepare:fixtures": "node scripts/prepare-fixtures.mjs", "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && nuxt prepare client && pnpm run prepare:fixtures && pnpm run build:client", "release": "pnpm build && bumpp -x \"npx changelogen --output=CHANGELOG.md\"", - "typecheck": "nuxt-module-build prepare && nuxt typecheck", + "typecheck": "pnpm stub && nuxt typecheck", "test": "pnpm run prepare:fixtures && vitest", "test:chrome": "HAS_CHROME=true vitest", + "test:packed-install": "node scripts/smoke-packed-install.mjs", "test:takumi:e2e": "node scripts/test-takumi-e2e.mjs", "test:takumi:e2e:matrix": "node scripts/test-takumi-e2e.mjs --matrix", "test:takumi:e2e:compare": "node scripts/test-takumi-e2e.mjs --compare" @@ -75,7 +76,10 @@ "@takumi-rs/core": "^1.0.0-beta.3 || ^2.0.0-rc.5", "@takumi-rs/wasm": "^1.0.0-beta.3 || ^2.0.0-rc.5", "@unhead/vue": "^2.0.5 || ^3.0.0", + "@unocss/config": "catalog:", + "@unocss/core": "catalog:", "fontless": "^0.2.0", + "lightningcss": "catalog:", "nitropack": "catalog:", "playwright-core": "^1.50.0", "satori": ">=0.19.2", @@ -98,9 +102,18 @@ "@takumi-rs/wasm": { "optional": true }, + "@unocss/config": { + "optional": true + }, + "@unocss/core": { + "optional": true + }, "fontless": { "optional": true }, + "lightningcss": { + "optional": true + }, "nitropack": { "optional": true }, @@ -126,16 +139,12 @@ "dependencies": { "@clack/prompts": "catalog:", "@nuxt/kit": "catalog:", - "@unocss/config": "catalog:", - "@unocss/core": "catalog:", "@vue/compiler-sfc": "catalog:", "chrome-launcher": "catalog:", "consola": "catalog:", - "culori": "catalog:", "defu": "catalog:", "devalue": "catalog:", "exsolve": "catalog:", - "lightningcss": "catalog:", "magic-string": "catalog:", "magicast": "catalog:", "mocked-exports": "catalog:", @@ -152,7 +161,6 @@ "std-env": "catalog:", "strip-literal": "catalog:", "tinyexec": "catalog:", - "tinyglobby": "catalog:", "ufo": "catalog:", "ultrahtml": "catalog:", "unplugin": "catalog:" @@ -189,6 +197,8 @@ "@takumi-rs/helpers-v1": "catalog:", "@takumi-rs/wasm": "catalog:", "@takumi-rs/wasm-v1": "catalog:", + "@unocss/config": "catalog:", + "@unocss/core": "catalog:", "@unocss/nuxt": "catalog:", "@unocss/preset-icons": "catalog:", "@unocss/preset-wind": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09b097c2a..737929f5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,6 @@ catalogs: consola: specifier: ^3.4.2 version: 3.4.2 - culori: - specifier: ^4.0.2 - version: 4.0.2 defu: specifier: ^6.1.7 version: 6.1.7 @@ -267,9 +264,6 @@ catalogs: tinyexec: specifier: ^1.2.4 version: 1.2.4 - tinyglobby: - specifier: ^0.2.17 - version: 0.2.17 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -327,12 +321,6 @@ importers: '@unhead/vue': specifier: ^3.1.7 version: 3.1.7(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(lightningcss@1.32.0)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) - '@unocss/config': - specifier: 'catalog:' - version: 66.7.4 - '@unocss/core': - specifier: 'catalog:' - version: 66.7.4 '@vue/compiler-sfc': specifier: 'catalog:' version: 3.5.39 @@ -342,9 +330,6 @@ importers: consola: specifier: 'catalog:' version: 3.4.2 - culori: - specifier: 'catalog:' - version: 4.0.2 defu: specifier: 'catalog:' version: 6.1.7 @@ -354,9 +339,6 @@ importers: exsolve: specifier: 'catalog:' version: 1.1.0 - lightningcss: - specifier: 'catalog:' - version: 1.32.0 magic-string: specifier: 'catalog:' version: 0.30.21 @@ -405,9 +387,6 @@ importers: tinyexec: specifier: 'catalog:' version: 1.2.4 - tinyglobby: - specifier: 'catalog:' - version: 0.2.17 ufo: specifier: 'catalog:' version: 1.6.4 @@ -483,7 +462,7 @@ importers: version: 12.1.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@nuxtjs/i18n': specifier: 'catalog:' - version: 10.4.0(@vue/compiler-dom@3.5.38)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.6.0(jiti@2.7.0))(ioredis@5.11.1)(magicast@0.5.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) + version: 10.4.0(@vue/compiler-dom@3.5.39)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.6.0(jiti@2.7.0))(ioredis@5.11.1)(magicast@0.5.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) '@nuxtjs/tailwindcss': specifier: 'catalog:' version: 7.0.0-beta.1(magicast@0.5.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) @@ -514,6 +493,12 @@ importers: '@takumi-rs/wasm-v1': specifier: 'catalog:' version: '@takumi-rs/wasm@1.8.7' + '@unocss/config': + specifier: 'catalog:' + version: 66.7.4 + '@unocss/core': + specifier: 'catalog:' + version: 66.7.4 '@unocss/nuxt': specifier: 'catalog:' version: 66.7.4(esbuild@0.28.1)(magicast@0.5.3)(rollup@4.61.1)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) @@ -565,6 +550,9 @@ importers: jest-image-snapshot: specifier: 'catalog:' version: 6.5.2 + lightningcss: + specifier: 'catalog:' + version: 1.32.0 nitropack: specifier: 'catalog:' version: 2.13.4(better-sqlite3@12.11.1)(oxc-parser@0.138.0)(srvx@0.11.17)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) @@ -5586,15 +5574,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@vue/compiler-core@3.5.38': - resolution: {integrity: sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==} - '@vue/compiler-core@3.5.39': resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==} - '@vue/compiler-dom@3.5.38': - resolution: {integrity: sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==} - '@vue/compiler-dom@3.5.39': resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==} @@ -6451,10 +6433,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - culori@4.0.2: - resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - cwise-compiler@1.1.3: resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==} @@ -10588,10 +10566,6 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} - unplugin@3.0.0: - resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} - engines: {node: ^20.19.0 || >=22.12.0} - unplugin@3.3.0: resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -12368,12 +12342,12 @@ snapshots: '@intlify/shared@11.4.5': {} - '@intlify/unplugin-vue-i18n@11.2.3(@vue/compiler-dom@3.5.38)(eslint@10.6.0(jiti@2.7.0))(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3))': + '@intlify/unplugin-vue-i18n@11.2.3(@vue/compiler-dom@3.5.39)(eslint@10.6.0(jiti@2.7.0))(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@intlify/bundle-utils': 11.2.3(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3))) '@intlify/shared': 11.4.5 - '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.4.5)(@vue/compiler-dom@3.5.38)(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3)) + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.4.5)(@vue/compiler-dom@3.5.39)(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3)) '@rollup/pluginutils': 5.4.0(rollup@4.61.1) '@typescript-eslint/scope-manager': 8.61.0 '@typescript-eslint/typescript-estree': 8.61.0(typescript@6.0.3) @@ -12397,12 +12371,12 @@ snapshots: '@intlify/utils@0.14.1': {} - '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.4.5)(@vue/compiler-dom@3.5.38)(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3))': + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.4.5)(@vue/compiler-dom@3.5.39)(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3))': dependencies: '@babel/parser': 7.29.7 optionalDependencies: '@intlify/shared': 11.4.5 - '@vue/compiler-dom': 3.5.38 + '@vue/compiler-dom': 3.5.39 vue: 3.5.39(typescript@6.0.3) vue-i18n: 11.4.5(vue@3.5.39(typescript@6.0.3)) @@ -12967,7 +12941,7 @@ snapshots: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.4.8(magicast@0.5.3) '@unhead/vue': 3.1.7(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(lightningcss@1.32.0)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) - '@vue/shared': 3.5.38 + '@vue/shared': 3.5.39 consola: 3.4.2 defu: 6.1.7 destr: 2.0.5 @@ -13339,12 +13313,12 @@ snapshots: - eslint-import-resolver-webpack - supports-color - '@nuxtjs/i18n@10.4.0(@vue/compiler-dom@3.5.38)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.6.0(jiti@2.7.0))(ioredis@5.11.1)(magicast@0.5.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15))': + '@nuxtjs/i18n@10.4.0(@vue/compiler-dom@3.5.39)(db0@0.3.4(better-sqlite3@12.11.1))(esbuild@0.28.1)(eslint@10.6.0(jiti@2.7.0))(ioredis@5.11.1)(magicast@0.5.3)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.3))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15))': dependencies: '@intlify/core': 11.4.5 '@intlify/h3': 0.7.4 '@intlify/shared': 11.4.5 - '@intlify/unplugin-vue-i18n': 11.2.3(@vue/compiler-dom@3.5.38)(eslint@10.6.0(jiti@2.7.0))(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3)) + '@intlify/unplugin-vue-i18n': 11.2.3(@vue/compiler-dom@3.5.39)(eslint@10.6.0(jiti@2.7.0))(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vue-i18n@11.4.5(vue@3.5.39(typescript@6.0.3)))(vue@3.5.39(typescript@6.0.3)) '@intlify/utils': 0.14.1 '@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.61.1) '@nuxt/kit': 4.4.8(magicast@0.5.3) @@ -15794,7 +15768,7 @@ snapshots: birpc: 4.0.0 devframe: 0.5.4(crossws@0.4.6(srvx@0.11.17))(esbuild@0.28.1)(rollup@4.61.1)(typescript@6.0.3)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) mlly: 1.8.2 - nostics: 0.3.0 + nostics: 0.3.0(esbuild@0.28.1)(rollup@4.61.1)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) pathe: 2.0.3 perfect-debounce: 2.1.0 tinyexec: 1.2.4 @@ -16030,7 +16004,7 @@ snapshots: '@babel/types': 7.29.7 '@vue/babel-helper-vue-transform-on': 2.0.1 '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.7) - '@vue/shared': 3.5.38 + '@vue/shared': 3.5.39 optionalDependencies: '@babel/core': 7.29.7 transitivePeerDependencies: @@ -16047,14 +16021,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vue/compiler-core@3.5.38': - dependencies: - '@babel/parser': 7.29.7 - '@vue/shared': 3.5.38 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - '@vue/compiler-core@3.5.39': dependencies: '@babel/parser': 7.29.7 @@ -16063,11 +16029,6 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.38': - dependencies: - '@vue/compiler-core': 3.5.38 - '@vue/shared': 3.5.38 - '@vue/compiler-dom@3.5.39': dependencies: '@vue/compiler-core': 3.5.39 @@ -16123,8 +16084,8 @@ snapshots: '@vue/language-core@3.3.4': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.38 - '@vue/shared': 3.5.38 + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -16133,8 +16094,8 @@ snapshots: '@vue/language-core@3.3.6': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.38 - '@vue/shared': 3.5.38 + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -16999,8 +16960,6 @@ snapshots: csstype@3.2.3: {} - culori@4.0.2: {} - cwise-compiler@1.1.3: dependencies: uniq: 1.0.1 @@ -20049,11 +20008,21 @@ snapshots: - vite - webpack - nostics@0.3.0: + nostics@0.3.0(esbuild@0.28.1)(rollup@4.61.1)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)): dependencies: magic-string: 0.30.21 oxc-parser: 0.132.0 - unplugin: 3.0.0 + unplugin: 3.3.0(esbuild@0.28.1)(rollup@4.61.1)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - esbuild + - rolldown + - rollup + - unloader + - vite + - webpack npm-run-path@5.3.0: dependencies: @@ -22839,12 +22808,6 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unplugin@3.0.0: - dependencies: - '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.4 - webpack-virtual-modules: 0.6.2 - unplugin@3.3.0(esbuild@0.28.0)(rollup@4.61.1)(vite@8.0.6(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(webpack@5.107.2(esbuild@0.28.1)(lightningcss@1.32.0)(postcss@8.5.15)): dependencies: '@jridgewell/remapping': 2.3.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 785f6eebb..30c5b7815 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -125,7 +125,6 @@ catalog: bumpp: ^11.1.0 chrome-launcher: ^1.2.1 consola: ^3.4.2 - culori: ^4.0.2 defu: ^6.1.7 devalue: ^5.8.1 eslint: ^10.6.0 @@ -164,7 +163,6 @@ catalog: strip-literal: ^3.1.0 tailwindcss: ^4.3.2 tinyexec: ^1.2.4 - tinyglobby: ^0.2.17 typescript: ^6.0.3 ufo: ^1.6.4 ultrahtml: ^1.6.0 diff --git a/scripts/smoke-packed-install.mjs b/scripts/smoke-packed-install.mjs new file mode 100644 index 000000000..184b7521d --- /dev/null +++ b/scripts/smoke-packed-install.mjs @@ -0,0 +1,293 @@ +import { spawn } from 'node:child_process' +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const rootPackage = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) +const keepTemp = process.argv.includes('--keep') +const packageManagers = process.argv + .find(arg => arg.startsWith('--pm=')) + ?.slice('--pm='.length) + .split(',') + .map(pm => pm.trim()) + .filter(Boolean) || ['pnpm', 'npm'] +const tempRoot = await mkdtemp(join(tmpdir(), 'og-image-packed-smoke-')) +const optionalizedPackages = [ + '@unocss/config', + '@unocss/core', + 'culori', + 'lightningcss', + 'tinyglobby', +] +const RE_ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g') + +function log(message) { + console.log(`[packed-smoke] ${message}`) +} + +function run(cwd, command, args, options = {}) { + return new Promise((resolveRun, reject) => { + const child = spawn(command, args, { + cwd, + stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit', + env: { + ...process.env, + CI: '1', + ...options.env, + }, + }) + let output = '' + if (options.capture) { + child.stdout.on('data', chunk => output += chunk) + child.stderr.on('data', chunk => output += chunk) + } + child.on('error', reject) + child.on('close', (code, signal) => { + if (code === 0) { + resolveRun(output) + return + } + const details = signal ? `signal ${signal}` : `exit code ${code}` + reject(new Error(`${command} ${args.join(' ')} failed with ${details}\n${output}`)) + }) + }) +} + +function assert(condition, message) { + if (!condition) + throw new Error(message) +} + +function assertNoWarnings(output, label) { + const lines = output + .replace(RE_ANSI, '') + .split('\n') + .map(line => line.trim()) + .filter(Boolean) + const warningLines = lines.filter(line => + /\b(?:warn|warning|error|failed)\b/i.test(line) + || /missing peer|unmet peer|peer dep/i.test(line), + ) + assert(warningLines.length === 0, `${label} emitted warning/error output:\n${warningLines.join('\n')}\n\nFull output:\n${output}`) +} + +function assertNoPeerWarnings(output, label) { + const lines = output + .replace(RE_ANSI, '') + .split('\n') + .map(line => line.trim()) + .filter(Boolean) + const peerWarningLines = lines.filter(line => /missing peer|unmet peer|peer dep/i.test(line)) + assert(peerWarningLines.length === 0, `${label} emitted peer dependency warnings:\n${peerWarningLines.join('\n')}\n\nFull output:\n${output}`) +} + +async function runClean(cwd, command, args, label) { + const output = await run(cwd, command, args, { capture: true }) + assertNoWarnings(output, label) + return output +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +async function listFiles(dir) { + const entries = await readdir(dir, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + const path = join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...await listFiles(path)) + continue + } + files.push(path) + } + return files +} + +async function packModule() { + log('packing nuxt-og-image') + await run(root, 'pnpm', ['pack', '--json', '--pack-destination', tempRoot], { capture: true }) + const files = await readdir(tempRoot) + const tarball = files.find(file => /^nuxt-og-image-.+\.tgz$/.test(file)) + assert(tarball, 'pnpm pack did not produce a nuxt-og-image tarball') + return join(tempRoot, tarball) +} + +async function getPackageManagerField(pm) { + if (pm === 'pnpm') + return rootPackage.packageManager + const version = (await run(root, pm, ['--version'], { capture: true })).trim() + return `${pm}@${version}` +} + +async function createApp(pm, name, dependencies, nuxtConfig) { + const appDir = join(tempRoot, name) + await mkdir(appDir, { recursive: true }) + await writeFile(join(appDir, 'package.json'), `${JSON.stringify({ + type: 'module', + packageManager: await getPackageManagerField(pm), + dependencies, + }, null, 2)}\n`) + if (pm === 'pnpm') { + await writeFile(join(appDir, 'pnpm-workspace.yaml'), [ + 'packages:', + ' - .', + 'onlyBuiltDependencies:', + ' - "@parcel/watcher"', + ' - esbuild', + 'allowBuilds:', + ' "@parcel/watcher": true', + ' esbuild: true', + 'minimumReleaseAgeExclude:', + ' - nuxtseo-shared@5.3.2', + '', + ].join('\n')) + } + await writeFile(join(appDir, 'nuxt.config.ts'), `${nuxtConfig.trim()}\n`) + return appDir +} + +async function install(pm, appDir) { + const args = pm === 'pnpm' + ? ['install', '--strict-peer-dependencies', '--reporter=append-only'] + : pm === 'npm' + ? ['install', '--strict-peer-deps', '--loglevel=warn'] + : ['install'] + const output = await run(appDir, pm, args, { capture: true }) + assertNoPeerWarnings(output, `${pm} install`) +} + +async function execPackage(pm, appDir, args, label) { + if (pm === 'bun') + return await runClean(appDir, 'bunx', ['--no-install', ...args], label) + + const commandArgs = pm === 'pnpm' + ? ['exec', ...args] + : ['exec', '--', ...args] + return await runClean(appDir, pm, commandArgs, label) +} + +async function readInstalledPackage(appDir) { + return JSON.parse(await readFile(join(appDir, 'node_modules/nuxt-og-image/package.json'), 'utf8')) +} + +function assertPackageMetadata(pkg) { + for (const name of ['culori', 'tinyglobby']) { + assert(!pkg.dependencies?.[name], `${name} should not be a runtime dependency`) + assert(!pkg.peerDependencies?.[name], `${name} should not be a peer dependency`) + } + + for (const name of ['@unocss/core', '@unocss/config', 'lightningcss']) { + assert(!pkg.dependencies?.[name], `${name} should not be a runtime dependency`) + assert(pkg.peerDependencies?.[name], `${name} should be declared as an optional peer`) + assert(pkg.peerDependenciesMeta?.[name]?.optional === true, `${name} peer should be optional`) + } +} + +async function assertNoDirectDistImports(appDir) { + const distDir = join(appDir, 'node_modules/nuxt-og-image/dist') + const files = (await listFiles(distDir)) + .filter(file => /\.(?:cjs|mjs|js)$/.test(file)) + + for (const file of files) { + const source = await readFile(file, 'utf8') + for (const name of optionalizedPackages) { + const escaped = escapeRegExp(name) + const directImport = new RegExp( + `(?:from\\s+['"]${escaped}(?:/[^'"]*)?['"]|import\\(\\s*['"]${escaped}(?:/[^'"]*)?['"]\\s*\\)|require\\(\\s*['"]${escaped}(?:/[^'"]*)?['"]\\s*\\))`, + ) + assert(!directImport.test(source), `${name} is still directly imported by ${file}`) + } + } +} + +async function assertNuxtOnlyApp(pm, tarball) { + log(`checking clean Nuxt-only app with ${pm}`) + const appDir = await createApp(pm, `${pm}-nuxt-only`, { + 'nuxt': '4.4.8', + 'nuxt-og-image': `file:${tarball}`, + }, ` + export default defineNuxtConfig({ + modules: ['nuxt-og-image'], + }) + `) + + await install(pm, appDir) + assertPackageMetadata(await readInstalledPackage(appDir)) + await assertNoDirectDistImports(appDir) + await execPackage(pm, appDir, ['nuxi', 'prepare'], `${pm} nuxi prepare`) + await execPackage(pm, appDir, ['nuxt-og-image', 'migrate', 'v6', '--dry-run', '--yes'], `${pm} nuxt-og-image migrate`) +} + +async function getSharedExports(appDir) { + const unoChunk = join(appDir, 'node_modules/nuxt-og-image/dist/chunks/uno.mjs') + const { resolveOptionalModulePath, importResolvedModule } = await import(pathToFileURL(unoChunk).href) + assert( + typeof resolveOptionalModulePath === 'function' && typeof importResolvedModule === 'function', + `expected optional-module resolvers to be exported from ${unoChunk}`, + ) + return { resolveOptionalModulePath, importResolvedModule } +} + +async function assertUnoApp(pm, tarball) { + log(`checking Uno app with ${pm} without direct Uno core/config or lightningcss deps`) + const appDir = await createApp(pm, `${pm}-unocss`, { + 'nuxt': '4.4.8', + '@unocss/nuxt': '66.7.4', + 'nuxt-og-image': `file:${tarball}`, + }, ` + export default defineNuxtConfig({ + modules: ['@unocss/nuxt', 'nuxt-og-image'], + }) + `) + await writeFile(join(appDir, 'uno.config.ts'), 'export default {}\n') + + await install(pm, appDir) + await execPackage(pm, appDir, ['nuxi', 'prepare'], `${pm} nuxi prepare with UnoCSS`) + + const pkg = JSON.parse(await readFile(join(appDir, 'package.json'), 'utf8')) + for (const name of ['@unocss/core', '@unocss/config', 'lightningcss']) + assert(!pkg.dependencies?.[name], `${name} should not be installed directly in the smoke app`) + + const { resolveOptionalModulePath, importResolvedModule } = await getSharedExports(appDir) + const rootDir = appDir + const [corePath, configPath, presetPath, lightningPath] = await Promise.all([ + resolveOptionalModulePath('@unocss/core', rootDir, ['@unocss/nuxt', 'unocss']), + resolveOptionalModulePath('@unocss/config', rootDir, ['@unocss/nuxt', 'unocss']), + resolveOptionalModulePath('@unocss/preset-wind', rootDir, ['@unocss/nuxt', 'unocss']), + resolveOptionalModulePath('lightningcss', rootDir, ['vite', '@nuxt/fonts', 'fontless', '@unhead/bundler']), + ]) + assert(corePath, 'expected @unocss/core to resolve through @unocss/nuxt') + assert(configPath, 'expected @unocss/config to resolve through @unocss/nuxt') + assert(presetPath, 'expected @unocss/preset-wind to resolve through @unocss/nuxt') + if (pm === 'pnpm') + assert(!lightningPath, 'pnpm smoke app unexpectedly resolved lightningcss') + + const { createUnoProvider, setUnoConfig, setUnoRootDir } = await import(pathToFileURL(join(appDir, 'node_modules/nuxt-og-image/dist/chunks/uno.mjs')).href) + const { presetWind } = await importResolvedModule('@unocss/preset-wind', presetPath) + + setUnoRootDir(rootDir) + setUnoConfig({ presets: [presetWind()] }) + const provider = createUnoProvider({ unocssCorePath: corePath, unocssConfigPath: configPath }) + const styles = await provider.resolveClassesToStyles(['text-red-500', 'font-bold']) + assert(styles['text-red-500']?.color, 'Uno provider did not resolve text-red-500') + assert(styles['font-bold']?.['font-weight'] === '700', 'Uno provider did not resolve font-bold') +} + +try { + const tarball = await packModule() + for (const pm of packageManagers) { + await assertNuxtOnlyApp(pm, tarball) + await assertUnoApp(pm, tarball) + } + log('passed') +} +finally { + if (keepTemp) + log(`kept temp directory ${tempRoot}`) + else + await rm(tempRoot, { recursive: true, force: true }) +} diff --git a/src/build/css/css-utils.ts b/src/build/css/css-utils.ts index 55704a378..a39b36a2a 100644 --- a/src/build/css/css-utils.ts +++ b/src/build/css/css-utils.ts @@ -2,6 +2,7 @@ import type { TemplateChildNode } from '@vue/compiler-core' import { logger } from '../../runtime/logger' import { GRADIENT_COLOR_SPACE_RE, resolveColorMix } from '../../runtime/server/og-image/utils/css' import { RE_WHITESPACE } from '../../util' +import { importOptionalPeer } from '../optional-module' const _warnedVars = new Set() @@ -39,21 +40,34 @@ const RE_COLOR_PROP = /color|fill|stroke|outline|border|background|caret|accent/ // Lightning CSS lazy loading // ============================================================================ -let transform: typeof import('lightningcss').transform -let Features: typeof import('lightningcss').Features +let lightningCssPath: string | undefined +let transform: typeof import('lightningcss').transform | undefined +let Features: typeof import('lightningcss').Features | undefined const LEGACY_TARGETS = { chrome: 60 << 16, safari: 10 << 16, } +export function setLightningCssPath(path?: string) { + if (path === lightningCssPath) + return + lightningCssPath = path + transform = undefined + Features = undefined +} + export async function loadLightningCss() { - if (!transform) { - const lcss = await import('lightningcss') + if (!transform || !Features) { + const lcss = await importOptionalPeer( + 'lightningcss', + lightningCssPath, + 'advanced OG image CSS parsing, color downleveling, or @font-face extraction', + ) transform = lcss.transform Features = lcss.Features } - return { transform, Features } + return { transform: transform!, Features: Features! } } /** @@ -141,8 +155,13 @@ function walkClassRules(css: string, onRule: (className: string, body: string) = * Simplify CSS using Lightning CSS with minification. */ export async function simplifyCss(css: string): Promise { - const result = await transformWithLightningCss(css, { minify: true }) - return result.code.toString() + try { + const result = await transformWithLightningCss(css, { minify: true }) + return result.code.toString() + } + catch { + return css + } } /** diff --git a/src/build/css/providers/tw4.ts b/src/build/css/providers/tw4.ts index 12d531ea1..be4621f56 100644 --- a/src/build/css/providers/tw4.ts +++ b/src/build/css/providers/tw4.ts @@ -11,7 +11,6 @@ import { extractPerClassVars, extractUniversalVars, extractVarsFromCss, - loadLightningCss, postProcessStyles, resolveExtractedVars, resolveVarsDeep, @@ -30,8 +29,7 @@ let twColors: Record> async function loadTw4Deps() { if (!compile) { - const [, tailwindModule, colorsModule] = await Promise.all([ - loadLightningCss(), + const [tailwindModule, colorsModule] = await Promise.all([ import('tailwindcss'), import('tailwindcss/colors'), ]) diff --git a/src/build/css/providers/uno.ts b/src/build/css/providers/uno.ts index b4b7d49ec..f1268ceef 100644 --- a/src/build/css/providers/uno.ts +++ b/src/build/css/providers/uno.ts @@ -1,12 +1,20 @@ -import type { UserConfig } from '@unocss/core' import type { CssProvider } from '../css-provider' import type { ExtractedCssVars } from '../css-utils' import { readFile } from 'node:fs/promises' import { defu } from 'defu' import { logger } from '../../../runtime/logger' +import { importOptionalPeer } from '../../optional-module' import { extractVariantBaseClasses, resolveVariantPrefixes } from '../css-provider' import { extractClassStyles, extractCssVars, extractPerClassVars, extractPropertyInitialValues, extractUniversalVars, extractVarsFromCss, postProcessStyles, resolveExtractedVars, simplifyCss } from '../css-utils' +// Re-exported so the packed-install smoke test can exercise the shipped +// resolver against a real consumer app's node_modules. +export { importResolvedModule, resolveOptionalModulePath } from '../../optional-module' + +type UserConfig = import('@unocss/core').UserConfig +type UnoCoreModule = typeof import('@unocss/core') +type UnoConfigModule = typeof import('@unocss/config') + const RE_SVG_DATA_URL = /url\("data:image\/svg\+xml[;,]([^"]+)"\)/ const RE_UTF8_PREFIX = /^utf8,/ const RE_VIEWBOX = /viewBox=['"](\d+)\s+(\d+)\s+(\d+)\s+(\d+)['"]/ @@ -15,12 +23,16 @@ const RE_HEIGHT_ATTR = /\bheight=['"](\d+)['"]/ const RE_SVG_BODY = /]*>([\s\S]*)<\/svg>/ export interface UnoProviderOptions { + unocssCorePath?: string + unocssConfigPath?: string resolveCssPath?: () => Promise } // State let unoConfig: UserConfig | null = null let rootDir: string | null = null +let unocssCorePath: string | undefined +let unocssConfigPath: string | undefined type UnoGen = Awaited> let generator: UnoGen | null = null let pendingGenerator: Promise | null = null @@ -57,6 +69,16 @@ export function clearUnoCache(): void { cachedExtracted = null } +function setUnoModulePaths(options?: Pick): void { + const nextCorePath = options?.unocssCorePath + const nextConfigPath = options?.unocssConfigPath + if (nextCorePath === unocssCorePath && nextConfigPath === unocssConfigPath) + return + unocssCorePath = nextCorePath + unocssConfigPath = nextConfigPath + clearUnoCache() +} + async function getGenerator(): Promise { if (generator) return generator @@ -68,12 +90,13 @@ async function getGenerator(): Promise { pendingGenerator = (async () => { let loadedConfig: UserConfig | undefined try { - const { loadConfig } = await import('@unocss/config') + const { loadConfig } = await importOptionalPeer('@unocss/config', unocssConfigPath, 'UnoCSS class extraction with OG images') const result = await loadConfig(rootDir || process.cwd()) loadedConfig = result.config } catch (e) { - logger.warn('[og-image UnoCSS] Failed to load uno.config.ts:', (e as Error).message) + if (!unoConfig) + logger.warn('[og-image UnoCSS] Failed to load uno.config.ts:', (e as Error).message) } const finalConfig = loadedConfig @@ -84,7 +107,7 @@ async function getGenerator(): Promise { throw new Error('UnoCSS config not found. Ensure uno.config.ts exists or @unocss/nuxt is loaded.') } - const { createGenerator } = await import('@unocss/core') + const { createGenerator } = await importOptionalPeer('@unocss/core', unocssCorePath, 'UnoCSS class extraction with OG images') generator = await createGenerator(finalConfig) return generator })() @@ -130,6 +153,8 @@ function resolveUserVars(extracted: ExtractedCssVars, rootAttrs: Record { + const rootUrl = directoryToURL(rootDir) + const direct = await tryResolveModule(id, rootUrl) + if (direct) + return direct + + for (const pkg of fallbackPackages) { + const pkgJson = await tryResolveModule(`${pkg}/package.json`, rootUrl) + if (!pkgJson) + continue + const nested = await tryResolveModule(id, directoryToURL(dirname(pkgJson))) + if (nested) + return nested + } +} + +export async function importResolvedModule(id: string, resolvedPath?: string): Promise { + return await import(resolvedPath ? pathToFileURL(resolvedPath).href : id) as T +} + +export async function importOptionalPeer(id: string, resolvedPath: string | undefined, usage: string): Promise { + try { + return await importResolvedModule(id, resolvedPath) + } + catch (cause) { + throw new Error( + `[nuxt-og-image] Missing optional peer dependency "${id}". Install it to use ${usage}.`, + { cause }, + ) + } +} diff --git a/src/cli.ts b/src/cli.ts index 6c93842e9..7a8f80bde 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,6 @@ import { parseAndWalk } from 'oxc-walker' import { basename, dirname, join, relative, resolve } from 'pathe' import { ELEMENT_NODE, parse as parseHtml, walkSync } from 'ultrahtml' import { migrateDefaultsComponent, migrateFontsConfig } from './migrations/fonts' -import { TAKUMI_CORE_INSTALL_SPEC, TAKUMI_WASM_INSTALL_SPEC } from './utils/dependencies' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -246,18 +245,13 @@ const EDGE_PRESETS = ['cloudflare', 'cloudflare-pages', 'cloudflare-module', 've // Get dependencies based on renderer and deployment target // Edge targets need both node (for local dev) and wasm (for production) versions -function getRendererDeps(renderer: RendererName, isEdge: boolean, options: { installSpec?: boolean } = {}): string[] { +function getRendererDeps(renderer: RendererName, isEdge: boolean): string[] { switch (renderer) { case 'satori': return isEdge ? ['satori', '@resvg/resvg-js', '@resvg/resvg-wasm'] : ['satori', '@resvg/resvg-js'] case 'takumi': - if (options.installSpec) { - return isEdge - ? [TAKUMI_CORE_INSTALL_SPEC, TAKUMI_WASM_INSTALL_SPEC] - : [TAKUMI_CORE_INSTALL_SPEC] - } return isEdge ? ['@takumi-rs/core', '@takumi-rs/wasm'] : ['@takumi-rs/core'] @@ -764,7 +758,7 @@ async function installRendererDeps(renderers: RendererName[], isEdge: boolean): const allDeps: string[] = [] for (const renderer of renderers) { - allDeps.push(...getRendererDeps(renderer, isEdge, { installSpec: true })) + allDeps.push(...getRendererDeps(renderer, isEdge)) } // Dedupe diff --git a/src/module.ts b/src/module.ts index c56a57f12..b4e43dbad 100644 --- a/src/module.ts +++ b/src/module.ts @@ -38,6 +38,7 @@ import { resolveFontFamilies, } from './build/fonts' import { setupGenerateHandler } from './build/generate' +import { resolveOptionalModulePath } from './build/optional-module' import { setupPrerenderHandler } from './build/prerender' import { extractPropNamesFromVue, loadSfcCompiler } from './build/props' import { resolveSigningSecret } from './build/signing-secret' @@ -550,6 +551,13 @@ export default defineNuxtModule({ // CSS framework detection - supports UnoCSS and Tailwind const { detectCssProvider } = await import('./build/css/css-provider') const cssFramework = detectCssProvider(nuxt) + const { setLightningCssPath } = await import('./build/css/css-utils') + setLightningCssPath(await resolveOptionalModulePath('lightningcss', nuxt.options.rootDir, [ + 'vite', + '@nuxt/fonts', + 'fontless', + '@unhead/bundler', + ])) // CSS provider for class resolution (UnoCSS or TW4) let cssProvider: CssProvider | undefined @@ -558,6 +566,10 @@ export default defineNuxtModule({ if (cssFramework === 'unocss') { logger.debug('UnoCSS detected, using UnoCSS provider for OG image styling') const { setUnoConfig, setUnoRootDir, createUnoProvider } = await import('./build/css/providers/uno') + const [unocssCorePath, unocssConfigPath] = await Promise.all([ + resolveOptionalModulePath('@unocss/core', nuxt.options.rootDir, ['@unocss/nuxt', 'unocss']), + resolveOptionalModulePath('@unocss/config', nuxt.options.rootDir, ['@unocss/nuxt', 'unocss']), + ]) // Set root directory for loading uno.config.ts setUnoRootDir(nuxt.options.rootDir) @@ -579,6 +591,8 @@ export default defineNuxtModule({ // Create the provider instance cssProvider = createUnoProvider({ + unocssCorePath, + unocssConfigPath, async resolveCssPath() { for (const cssEntry of nuxt.options.css) { // @ts-expect-error untyped