From 98570fe2080956bc22d5a9a691fa1f0f8c6b4051 Mon Sep 17 00:00:00 2001 From: harlan Date: Sun, 17 Mar 2024 21:41:53 +1100 Subject: [PATCH] feat: support bundling scripts --- README.md | 69 ++++++++++- client/app.vue | 30 +++-- eslint.config.js | 1 + package.json | 20 ++- pnpm-lock.yaml | 259 +++++++++++++++++++++++---------------- src/assets.ts | 122 ++++++++++++++++++ src/cache.ts | 31 +++++ src/logger.ts | 3 + src/module.ts | 31 ++++- src/plugins/transform.ts | 101 +++++++++++++++ src/runtime/types.ts | 4 +- vitest.config.ts | 10 +- 12 files changed, 548 insertions(+), 133 deletions(-) create mode 100644 src/assets.ts create mode 100644 src/cache.ts create mode 100644 src/logger.ts create mode 100644 src/plugins/transform.ts diff --git a/README.md b/README.md index 05e1852b..9c8b483c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Plus Nuxt goodies: - 🕵️ `createConsentTrigger` - Create a script trigger that you can resolve with a ref or a promise. - 🪵 DevTools integration - see all your loaded scripts with function logs +- ⏬ Serve third-party scripts from your own server ## Installation @@ -54,6 +55,66 @@ Nuxt Scripts was created to solve these issues and more with the goal of making ## Usage +### Getting Started + +To start using Nuxt Scripts, you can use the `useScript` composable to load your third-party scripts. + +```ts +useScript('https://cdn.jsdelivr.net/npm/js-confetti@latest/dist/js-confetti.browser.js') +``` + +See the Unhead [useScript](https://unhead.unjs.io/usage/composables/use-script) guide for next steps. + +### Bundling Scripts + +Bundling scripts can allow you to serve them from your own server, improving privacy and performance. It +can also help to get around ad blockers and other privacy tools when you need a script to load. + +You can opt-in to have your scripts bundled by using the `assetStrategy` option. As this is +analyzed at build time, you must define it statically. + +```ts +useScript('https://cdn.jsdelivr.net/npm/js-confetti@latest/dist/js-confetti.browser.js', { + assetStrategy: 'bundle' +}) +// js-confetti.browser.js will be downloaded and bundled with your app as a static asset +``` + +### Sending Page Events + +When using tracking scripts, it's common to send an event when the page changes. Due to Nuxt's head implementation being +async, the page title is not always available on route change immediately. + +`useAnalyticsPageEvent` solves this by providing you with the page title and path when they change. + +```ts +useAnalyticsPageEvent(({ title, path }) => { + // triggered on route change + gtag('event', 'page_view', { + page_title: title, + page_location: 'https://example.com', + page_path: path + }) +}) +``` + +### Privacy and Cookie Consent + +Nuxt Scripts provides a `createConsentTrigger` composable that allows you to load scripts based on the user's consent. + +```ts +const agreedToCookies = ref(false) +useScript('https://www.google-analytics.com/analytics.js', { + // will be loaded in when the ref is true + trigger: createConsentTrigger({ + honourDoNotTrack: true, // optional, disabled by default + consent: agreedToCookies + }) +}) +``` + +## API + ### `useScript` Please see the [useScript](https://unhead.unjs.io/usage/composables/use-script) documentation. @@ -88,17 +149,17 @@ useScript('https://www.google-analytics.com/analytics.js', { }) ``` -### `useTrackedPage` +### `useAnalyticsPageEvent` It's common when using tracking scripts to send an event when the page changes. Due to Nuxt's head implementation being async, it's not possible to send the page title on route change. -`useTrackedPage` solves this by providing you with the page title and path when they change. +`useAnalyticsPageEvent` solves this by providing you with the page title and path when they change. You can either provide a function to call on page change or use the ref that's returned. ```ts -useTrackedPage(({ title, path }) => { +useAnalyticsPageEvent(({ title, path }) => { gtag('event', 'page_view', { page_title: title, page_location: 'https://example.com', @@ -108,7 +169,7 @@ useTrackedPage(({ title, path }) => { ``` ```ts -const trackedPage = useTrackedPage() +const trackedPage = useAnalyticsPageEvent() watch(trackedPage, ({ title, path }) => { gtag('event', 'page_view', { page_title: title, diff --git a/client/app.vue b/client/app.vue index a1da55c9..d4295c72 100644 --- a/client/app.vue +++ b/client/app.vue @@ -1,5 +1,6 @@ diff --git a/eslint.config.js b/eslint.config.js index 865df582..ee6b0dea 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,5 +3,6 @@ import antfu from '@antfu/eslint-config' export default antfu({ rules: { 'no-use-before-define': 'off', + 'node/prefer-global/buffer': 'off', }, }) diff --git a/package.json b/package.json index a3cdae35..0f5b3278 100644 --- a/package.json +++ b/package.json @@ -52,21 +52,31 @@ "dependencies": { "@nuxt/devtools-kit": "^1.0.8", "@nuxt/devtools-ui-kit": "^1.0.8", - "@nuxt/kit": "^3.10.3", + "@nuxt/kit": "^3.11.0", + "consola": "^3.2.3", "defu": "^6.1.4", + "estree-walker": "^3.0.3", + "h3": "^1.11.1", + "magic-string": "^0.30.8", + "ofetch": "^1.3.3", + "ohash": "^1.1.3", + "pathe": "^1.1.2", "pkg-types": "^1.0.3", "shiki": "^1.2.0", - "sirv": "^2.0.4" + "sirv": "^2.0.4", + "ufo": "^1.5.1", + "unplugin": "^1.10.0", + "unstorage": "^1.10.2" }, "devDependencies": { - "@antfu/eslint-config": "2.8.1", + "@antfu/eslint-config": "2.8.3", "@nuxt/devtools-ui-kit": "^1.0.8", "@nuxt/module-builder": "^0.5.5", - "@nuxt/test-utils": "3.11.0", + "@nuxt/test-utils": "3.12.0", "@nuxt/ui": "2.14.2", "bumpp": "^9.4.0", "eslint": "8.57.0", - "nuxt": "^3.10.3", + "nuxt": "^3.11.0", "nuxt-icon": "^0.6.9", "playwright": "^1.42.1", "typescript": "^5.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3025929..358fab65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,16 +7,37 @@ settings: dependencies: '@nuxt/devtools-kit': specifier: ^1.0.8 - version: 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6) + version: 1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6) '@nuxt/devtools-ui-kit': specifier: ^1.0.8 - version: 1.0.8(@nuxt/devtools@1.0.8)(@vue/compiler-core@3.4.21)(nuxt@3.10.3)(postcss@8.4.35)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21)(webpack@5.90.3) + version: 1.0.8(@nuxt/devtools@1.0.8)(@vue/compiler-core@3.4.21)(nuxt@3.11.0)(postcss@8.4.35)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21)(webpack@5.90.3) '@nuxt/kit': - specifier: ^3.10.3 - version: 3.10.3(rollup@3.29.4) + specifier: ^3.11.0 + version: 3.11.0(rollup@3.29.4) + consola: + specifier: ^3.2.3 + version: 3.2.3 defu: specifier: ^6.1.4 version: 6.1.4 + estree-walker: + specifier: ^3.0.3 + version: 3.0.3 + h3: + specifier: ^1.11.1 + version: 1.11.1 + magic-string: + specifier: ^0.30.8 + version: 0.30.8 + ofetch: + specifier: ^1.3.3 + version: 1.3.3 + ohash: + specifier: ^1.1.3 + version: 1.1.3 + pathe: + specifier: ^1.1.2 + version: 1.1.2 pkg-types: specifier: ^1.0.3 version: 1.0.3 @@ -26,20 +47,29 @@ dependencies: sirv: specifier: ^2.0.4 version: 2.0.4 + ufo: + specifier: ^1.5.1 + version: 1.5.1 + unplugin: + specifier: ^1.10.0 + version: 1.10.0 + unstorage: + specifier: ^1.10.2 + version: 1.10.2(ioredis@5.3.2) devDependencies: '@antfu/eslint-config': - specifier: 2.8.1 - version: 2.8.1(@vue/compiler-sfc@3.4.21)(eslint@8.57.0)(typescript@5.4.2)(vitest@1.4.0) + specifier: 2.8.3 + version: 2.8.3(@vue/compiler-sfc@3.4.21)(eslint@8.57.0)(typescript@5.4.2)(vitest@1.4.0) '@nuxt/module-builder': specifier: ^0.5.5 - version: 0.5.5(@nuxt/kit@3.10.3)(nuxi@3.10.1)(typescript@5.4.2) + version: 0.5.5(@nuxt/kit@3.11.0)(nuxi@3.10.1)(typescript@5.4.2) '@nuxt/test-utils': - specifier: 3.11.0 - version: 3.11.0(h3@1.11.1)(rollup@3.29.4)(vite@5.1.6)(vitest@1.4.0)(vue-router@4.3.0)(vue@3.4.21) + specifier: 3.12.0 + version: 3.12.0(h3@1.11.1)(rollup@3.29.4)(vite@5.1.6)(vitest@1.4.0)(vue-router@4.3.0)(vue@3.4.21) '@nuxt/ui': specifier: 2.14.2 - version: 2.14.2(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21) + version: 2.14.2(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21) bumpp: specifier: ^9.4.0 version: 9.4.0 @@ -47,11 +77,11 @@ devDependencies: specifier: 8.57.0 version: 8.57.0 nuxt: - specifier: ^3.10.3 - version: 3.10.3(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) + specifier: ^3.11.0 + version: 3.11.0(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) nuxt-icon: specifier: ^0.6.9 - version: 0.6.9(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21) + version: 0.6.9(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21) playwright: specifier: ^1.42.1 version: 1.42.1 @@ -80,8 +110,8 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@antfu/eslint-config@2.8.1(@vue/compiler-sfc@3.4.21)(eslint@8.57.0)(typescript@5.4.2)(vitest@1.4.0): - resolution: {integrity: sha512-9fgSdaycCj4odiejWrCMET/Ub+dktRUSxFr8rMJ9SfiOlimav86SHo0myEtj14422yTrw8J9XkVUW6Q9ASt2Og==} + /@antfu/eslint-config@2.8.3(@vue/compiler-sfc@3.4.21)(eslint@8.57.0)(typescript@5.4.2)(vitest@1.4.0): + resolution: {integrity: sha512-M3l7KwDrtAaF90km8D0G0bhkEDl2/V4yh/Qi08uiFHoMAEzbqwF73HRIJxJgL1YwRcyUnJy7yEUkA04cI1GFEQ==} hasBin: true peerDependencies: '@unocss/eslint-plugin': '>=0.50.0' @@ -122,6 +152,7 @@ packages: dependencies: '@antfu/eslint-define-config': 1.23.0-2 '@antfu/install-pkg': 0.3.1 + '@clack/prompts': 0.7.0 '@eslint-types/jsdoc': 46.8.2-1 '@eslint-types/typescript-eslint': 7.0.2 '@eslint-types/unicorn': 51.0.1 @@ -152,7 +183,6 @@ packages: local-pkg: 0.5.0 parse-gitignore: 2.0.0 picocolors: 1.0.0 - prompts: 2.4.2 toml-eslint-parser: 0.9.3 vue-eslint-parser: 9.4.2(eslint@8.57.0) yaml-eslint-parser: 1.2.2 @@ -518,6 +548,23 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@clack/core@0.3.4: + resolution: {integrity: sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==} + dependencies: + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: true + + /@clack/prompts@0.7.0: + resolution: {integrity: sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==} + dependencies: + '@clack/core': 0.3.4 + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: true + bundledDependencies: + - is-unicode-supported + /@cloudflare/kv-asset-handler@0.3.1: resolution: {integrity: sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==} dependencies: @@ -1327,22 +1374,22 @@ packages: /@nuxt/devalue@2.0.2: resolution: {integrity: sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==} - /@nuxt/devtools-kit@1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6): + /@nuxt/devtools-kit@1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6): resolution: {integrity: sha512-j7bNZmoAXQ1a8qv6j6zk4c/aekrxYqYVQM21o/Hy4XHCUq4fajSgpoc8mjyWJSTfpkOmuLyEzMexpDWiIVSr6A==} peerDependencies: nuxt: ^3.9.0 vite: '*' dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) - '@nuxt/schema': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) + '@nuxt/schema': 3.11.0(rollup@3.29.4) execa: 7.2.0 - nuxt: 3.10.3(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) + nuxt: 3.11.0(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) vite: 5.1.6 transitivePeerDependencies: - rollup - supports-color - /@nuxt/devtools-ui-kit@1.0.8(@nuxt/devtools@1.0.8)(@vue/compiler-core@3.4.21)(nuxt@3.10.3)(postcss@8.4.35)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21)(webpack@5.90.3): + /@nuxt/devtools-ui-kit@1.0.8(@nuxt/devtools@1.0.8)(@vue/compiler-core@3.4.21)(nuxt@3.11.0)(postcss@8.4.35)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21)(webpack@5.90.3): resolution: {integrity: sha512-oPkyQ+nkvCvveWxHWAHpZt9uEycHFD00Rh46KYKe5KLl81Wr/L3KacIIYpiocPog0YZZhjvX5CmrIe8zXopNOA==} peerDependencies: '@nuxt/devtools': 1.0.8 @@ -1351,9 +1398,9 @@ packages: '@iconify-json/logos': 1.1.42 '@iconify-json/ri': 1.1.20 '@iconify-json/tabler': 1.1.108 - '@nuxt/devtools': 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6) - '@nuxt/devtools-kit': 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6) - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/devtools': 1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6) + '@nuxt/devtools-kit': 1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6) + '@nuxt/kit': 3.11.0(rollup@3.29.4) '@nuxtjs/color-mode': 3.3.2(rollup@3.29.4) '@unocss/core': 0.58.6 '@unocss/nuxt': 0.58.6(postcss@8.4.35)(rollup@3.29.4)(vite@5.1.6)(webpack@5.90.3) @@ -1363,7 +1410,7 @@ packages: '@unocss/reset': 0.58.6 '@vueuse/core': 10.9.0(vue@3.4.21) '@vueuse/integrations': 10.9.0(focus-trap@7.5.4)(vue@3.4.21) - '@vueuse/nuxt': 10.9.0(nuxt@3.10.3)(rollup@3.29.4)(vue@3.4.21) + '@vueuse/nuxt': 10.9.0(nuxt@3.11.0)(rollup@3.29.4)(vue@3.4.21) defu: 6.1.4 focus-trap: 7.5.4 splitpanes: 3.1.5 @@ -1408,7 +1455,7 @@ packages: rc9: 2.1.1 semver: 7.6.0 - /@nuxt/devtools@1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6): + /@nuxt/devtools@1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6): resolution: {integrity: sha512-o6aBFEBxc8OgVHV4OPe2g0q9tFIe9HiTxRiJnlTJ+jHvOQsBLS651ArdVtwLChf9UdMouFlpLLJ1HteZqTbtsQ==} hasBin: true peerDependencies: @@ -1416,9 +1463,9 @@ packages: vite: '*' dependencies: '@antfu/utils': 0.7.7 - '@nuxt/devtools-kit': 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6) + '@nuxt/devtools-kit': 1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6) '@nuxt/devtools-wizard': 1.0.8 - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) birpc: 0.2.17 consola: 3.2.3 destr: 2.0.3 @@ -1433,7 +1480,7 @@ packages: launch-editor: 2.6.1 local-pkg: 0.5.0 magicast: 0.3.3 - nuxt: 3.10.3(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) + nuxt: 3.11.0(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) nypm: 0.3.8 ohash: 1.1.3 pacote: 17.0.6 @@ -1443,11 +1490,11 @@ packages: rc9: 2.1.1 scule: 1.3.0 semver: 7.6.0 - simple-git: 3.22.0 + simple-git: 3.23.0 sirv: 2.0.4 unimport: 3.7.1(rollup@3.29.4) vite: 5.1.6 - vite-plugin-inspect: 0.8.3(@nuxt/kit@3.10.3)(rollup@3.29.4)(vite@5.1.6) + vite-plugin-inspect: 0.8.3(@nuxt/kit@3.11.0)(rollup@3.29.4)(vite@5.1.6) vite-plugin-vue-inspector: 4.0.2(vite@5.1.6) which: 3.0.1 ws: 8.16.0 @@ -1458,11 +1505,11 @@ packages: - supports-color - utf-8-validate - /@nuxt/kit@3.10.3(rollup@3.29.4): - resolution: {integrity: sha512-PUjYB9Mvx0qD9H1QZBwwtY4fLlCLET+Mm9BVqUOtXCaGoXd6u6BE4e/dGFPk2UEKkIcDGrUMSbqkHYvsEuK9NQ==} + /@nuxt/kit@3.11.0(rollup@3.29.4): + resolution: {integrity: sha512-uXpOnlQ+Y77Cux4s6IqPR5B4xx3QNOGrW/D41K1ByYmeagGvmVqI7gOiHJl+C1s9MX8Ky/STfcIMaozEvy9E6w==} engines: {node: ^14.18.0 || >=16.10.0} dependencies: - '@nuxt/schema': 3.10.3(rollup@3.29.4) + '@nuxt/schema': 3.11.0(rollup@3.29.4) c12: 1.10.0 consola: 3.2.3 defu: 6.1.4 @@ -1484,14 +1531,14 @@ packages: - rollup - supports-color - /@nuxt/module-builder@0.5.5(@nuxt/kit@3.10.3)(nuxi@3.10.1)(typescript@5.4.2): + /@nuxt/module-builder@0.5.5(@nuxt/kit@3.11.0)(nuxi@3.10.1)(typescript@5.4.2): resolution: {integrity: sha512-ifFfwA1rbSXSae25RmqA2kAbV3xoShZNrq1yK8VXB/EnIcDn4WiaYR1PytaSxIt5zsvWPn92BJXiIUBiMQZ0hw==} hasBin: true peerDependencies: '@nuxt/kit': ^3.8.2 nuxi: ^3.10.0 dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) citty: 0.1.6 consola: 3.2.3 mlly: 1.6.1 @@ -1504,8 +1551,8 @@ packages: - typescript dev: true - /@nuxt/schema@3.10.3(rollup@3.29.4): - resolution: {integrity: sha512-a4cYbeskEVBPazgAhvUGkL/j7ho/iPWMK3vCEm6dRMjSqHVEITRosrj0aMfLbRrDpTrMjlRs0ZitxiaUfE/p5Q==} + /@nuxt/schema@3.11.0(rollup@3.29.4): + resolution: {integrity: sha512-vonev7BhcVoXwpOUuyQJAvXQpzw0R1Xi/B/nG24ufCEpIfcwJr6ihhDRYFvQ8yIdxZMK7W8/K73vmUDJQ42dRw==} engines: {node: ^14.18.0 || >=16.10.0} dependencies: '@nuxt/ui-templates': 1.3.1 @@ -1527,7 +1574,7 @@ packages: resolution: {integrity: sha512-Ghv2MgWbJcUM9G5Dy3oQP0cJkUwEgaiuQxEF61FXJdn0a69Q4StZEP/hLF0MWPM9m6EvAwI7orxkJHM7MrmtVg==} hasBin: true dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) ci-info: 4.0.0 consola: 3.2.3 create-require: 1.1.1 @@ -1548,12 +1595,13 @@ packages: - rollup - supports-color - /@nuxt/test-utils@3.11.0(h3@1.11.1)(rollup@3.29.4)(vite@5.1.6)(vitest@1.4.0)(vue-router@4.3.0)(vue@3.4.21): - resolution: {integrity: sha512-9ovgpQZkZpVg/MhYVVn2169WjH/IL0XUqwGryTa/lkx0/BCi1LMVEp3HTPkmt4qbRcxitO+kL4vFqqrFGVaSVg==} + /@nuxt/test-utils@3.12.0(h3@1.11.1)(rollup@3.29.4)(vite@5.1.6)(vitest@1.4.0)(vue-router@4.3.0)(vue@3.4.21): + resolution: {integrity: sha512-Q3HP53TDIYeqHT65r31HZhK/gRwVBmchSdVj1tfiYECyqstckvsQ4Cyt/GX/XmD7cLdD3d5aHow8LaMfP+BSqQ==} engines: {node: ^14.18.0 || >=16.10.0} peerDependencies: '@cucumber/cucumber': ^10.3.1 '@jest/globals': ^29.5.0 + '@playwright/test': ^1.42.1 '@testing-library/vue': ^7.0.0 || ^8.0.1 '@vitest/ui': ^0.34.6 || ^1.0.0 '@vue/test-utils': ^2.4.2 @@ -1570,6 +1618,8 @@ packages: optional: true '@jest/globals': optional: true + '@playwright/test': + optional: true '@testing-library/vue': optional: true '@vitest/ui': @@ -1585,8 +1635,8 @@ packages: vitest: optional: true dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) - '@nuxt/schema': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) + '@nuxt/schema': 3.11.0(rollup@3.29.4) c12: 1.10.0 consola: 3.2.3 defu: 6.1.4 @@ -1621,7 +1671,7 @@ packages: /@nuxt/ui-templates@1.3.1: resolution: {integrity: sha512-5gc02Pu1HycOVUWJ8aYsWeeXcSTPe8iX8+KIrhyEtEoOSkY0eMBuo0ssljB8wALuEmepv31DlYe5gpiRwkjESA==} - /@nuxt/ui@2.14.2(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21): + /@nuxt/ui@2.14.2(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21): resolution: {integrity: sha512-xEtgnofE2A/Ou+Afv70m/hLtcfvLs41cs/qZHVcqErv6OO8uKyDETS5bKhiZGlDcI0wccTJq/ULaDUGuwnRN2g==} engines: {node: '>=v16.20.2'} dependencies: @@ -1629,7 +1679,7 @@ packages: '@headlessui/tailwindcss': 0.2.0(tailwindcss@3.4.1) '@headlessui/vue': 1.7.19(vue@3.4.21) '@iconify-json/heroicons': 1.1.20 - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) '@nuxtjs/color-mode': 3.3.2(rollup@3.29.4) '@nuxtjs/tailwindcss': 6.11.4(rollup@3.29.4) '@popperjs/core': 2.11.8 @@ -1642,11 +1692,11 @@ packages: '@vueuse/math': 10.9.0(vue@3.4.21) defu: 6.1.4 fuse.js: 6.6.2 - nuxt-icon: 0.6.9(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21) + nuxt-icon: 0.6.9(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21) ohash: 1.1.3 pathe: 1.1.2 scule: 1.3.0 - tailwind-merge: 2.2.1 + tailwind-merge: 2.2.2 tailwindcss: 3.4.1 transitivePeerDependencies: - '@vue/composition-api' @@ -1670,13 +1720,13 @@ packages: - vue dev: true - /@nuxt/vite-builder@3.10.3(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vue@3.4.21): - resolution: {integrity: sha512-BqkbrYkEk1AVUJleofbqTRV+ltf2p1CDjGDK78zENPCgrSABlj4F4oK8rze8vmRY5qoH7kMZxgMa2dXVXCp6OA==} + /@nuxt/vite-builder@3.11.0(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vue@3.4.21): + resolution: {integrity: sha512-DtTRz0kTwxeUTTNm/vAAWUhxIug5B2TNT77mGcqZD4yVFXn5xcQkc6nyXLaS/f1qqJvKaS0klWMAb/pwoPcweg==} engines: {node: ^14.18.0 || >=16.10.0} peerDependencies: vue: ^3.3.4 dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) '@rollup/plugin-replace': 5.0.5(rollup@3.29.4) '@vitejs/plugin-vue': 5.0.4(vite@5.1.6)(vue@3.4.21) '@vitejs/plugin-vue-jsx': 3.1.0(vite@5.1.6)(vue@3.4.21) @@ -1734,7 +1784,7 @@ packages: /@nuxtjs/color-mode@3.3.2(rollup@3.29.4): resolution: {integrity: sha512-BLpBfrYZngV2QWFQ4HNEFwAXa3Pno43Ge+2XHcZJTTa1Z4KzRLvOwku8yiyV3ovIaaXKGwduBdv3Z5Ocdp0/+g==} dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) lodash.template: 4.5.0 pathe: 1.1.2 transitivePeerDependencies: @@ -1744,7 +1794,7 @@ packages: /@nuxtjs/tailwindcss@6.11.4(rollup@3.29.4): resolution: {integrity: sha512-09cksgZD4seQj054Z/BeiwFg1bzQTol8KPulLDLGnmMTkEi21vj/z+WlXQRpVbN1GS9+oU9tcSsu2ufXCM3DBg==} dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) autoprefixer: 10.4.18(postcss@8.4.35) chokidar: 3.6.0 clear-module: 4.1.2 @@ -2647,38 +2697,38 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - /@unhead/dom@1.8.19: - resolution: {integrity: sha512-xoYOXd0wohxjLwD1m+FNZufIWNukg57tuchlk0FqX5GzFCn/sT1BT4jZV1JeWDWPxaPfJdLHotc0x06yKEAfLA==} + /@unhead/dom@1.8.20: + resolution: {integrity: sha512-TXRQSVbqBOQc02m3wxgj55m93U8a3WBHV9xJi2zVX/iHEJgeQbZMJ+rV0YJkHy2OHAC0MfjVQA5NDLaVwtromw==} dependencies: - '@unhead/schema': 1.8.19 - '@unhead/shared': 1.8.19 + '@unhead/schema': 1.8.20 + '@unhead/shared': 1.8.20 - /@unhead/schema@1.8.19: - resolution: {integrity: sha512-m7lwpjyzeVtiW7oRPyR0dfDLUr+YAj78CBoJOr2syeJ5/foOnsiaVbJETE6h/pFq4Ut6P7c/q5eqskoHcAjV/g==} + /@unhead/schema@1.8.20: + resolution: {integrity: sha512-n0e5jsKino8JTHc4wpr4l8MXXIrj0muYYAEVa0WSYkIVnMiBr1Ik3l6elhCr4fdSyJ3M2DQQleea/oZCr11XCw==} dependencies: hookable: 5.5.3 zhead: 2.2.4 - /@unhead/shared@1.8.19: - resolution: {integrity: sha512-v/8GBkGf7+CmZhtNGbBjQnwZ+jJTJtupddmKxPpPh7qzwPaJcACE8Tp6MypADQafZbM3D7iqyxbFqvIXklJoIQ==} + /@unhead/shared@1.8.20: + resolution: {integrity: sha512-J0fdtavcMtXcG0g9jmVW03toqfr8A0G7k+Q6jdpwuUPhWk/vhfZn3aiRV+F8LlU91c/AbGWDv8T1MrtMQbb0Sg==} dependencies: - '@unhead/schema': 1.8.19 + '@unhead/schema': 1.8.20 - /@unhead/ssr@1.8.19: - resolution: {integrity: sha512-fdtBNVHO69GkqMv7TwqQnAhiuP47zd9RRb50gcwV2+JMA18j1MlQXTkErpG6fPVg5Y+YNw4lJOXz0OBp4t30Eg==} + /@unhead/ssr@1.8.20: + resolution: {integrity: sha512-Cq1NcdYZ/IAkJ0muqdOBxJXb5dn+uV+RvIXDykRb9lGgriU/S0fzUw8XYTYMwLlvW6rSMrtRx319hz2D3ZrBkA==} dependencies: - '@unhead/schema': 1.8.19 - '@unhead/shared': 1.8.19 + '@unhead/schema': 1.8.20 + '@unhead/shared': 1.8.20 - /@unhead/vue@1.8.19(vue@3.4.21): - resolution: {integrity: sha512-WxGDDDPMnSQQrk/0G40XNsiHACQc1FSe2daie4WfHDr6sqjO6X3qBUEj7oQUDODRcUrO6m6TgHvfznNmGGr6UA==} + /@unhead/vue@1.8.20(vue@3.4.21): + resolution: {integrity: sha512-Lm6cnbX/QGCh+pxGN1Tl6LVXxYs5bLlN8APfI2rQ5kMNRE6Yy7r2eY5wCZ0SfsSRonqJxzVlgMMh8JPEY5d4GQ==} peerDependencies: vue: '>=2.7 || >=3' dependencies: - '@unhead/schema': 1.8.19 - '@unhead/shared': 1.8.19 + '@unhead/schema': 1.8.20 + '@unhead/shared': 1.8.20 hookable: 5.5.3 - unhead: 1.8.19 + unhead: 1.8.20 vue: 3.4.21(typescript@5.4.2) /@unocss/astro@0.58.6(rollup@3.29.4)(vite@5.1.6): @@ -2749,7 +2799,7 @@ packages: /@unocss/nuxt@0.58.6(postcss@8.4.35)(rollup@3.29.4)(vite@5.1.6)(webpack@5.90.3): resolution: {integrity: sha512-DDKnoLhp/1kVpWGCSn+7l9OkaKkMefjQsc0jM4B0e5ihCYeZgP3O7GeJdoX/wSj4QVoJQh4Prosyf8Ili0F8/A==} dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) '@unocss/config': 0.58.6 '@unocss/core': 0.58.6 '@unocss/preset-attributify': 0.58.6 @@ -3272,16 +3322,16 @@ packages: /@vueuse/metadata@10.9.0: resolution: {integrity: sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==} - /@vueuse/nuxt@10.9.0(nuxt@3.10.3)(rollup@3.29.4)(vue@3.4.21): + /@vueuse/nuxt@10.9.0(nuxt@3.11.0)(rollup@3.29.4)(vue@3.4.21): resolution: {integrity: sha512-nC4Efg28Q6E41fUD5R+zM9uT5c+NfaDzaJCpqaEV/qHj+/BNJmkDBK8POLIUsiVOY35d0oD/YxZ+eVizqWBZow==} peerDependencies: nuxt: ^3.0.0 dependencies: - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) '@vueuse/core': 10.9.0(vue@3.4.21) '@vueuse/metadata': 10.9.0 local-pkg: 0.5.0 - nuxt: 3.10.3(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) + nuxt: 3.11.0(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6) vue-demi: 0.14.7(vue@3.4.21) transitivePeerDependencies: - '@vue/composition-api' @@ -3664,7 +3714,7 @@ packages: postcss: ^8.1.0 dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001597 + caniuse-lite: 1.0.30001598 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -3722,7 +3772,7 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001597 + caniuse-lite: 1.0.30001598 electron-to-chromium: 1.4.708 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) @@ -3836,12 +3886,12 @@ packages: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} dependencies: browserslist: 4.23.0 - caniuse-lite: 1.0.30001597 + caniuse-lite: 1.0.30001598 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - /caniuse-lite@1.0.30001597: - resolution: {integrity: sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==} + /caniuse-lite@1.0.30001598: + resolution: {integrity: sha512-j8mQRDziG94uoBfeFuqsJUNECW37DXpnvhcMJMdlH2u3MRkq1sAI0LJcXP1i/Py0KbSIC4UDj8YHPrTn5YsL+Q==} /chai@4.4.1: resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} @@ -6763,13 +6813,13 @@ packages: optionalDependencies: fsevents: 2.3.3 - /nuxt-icon@0.6.9(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21): + /nuxt-icon@0.6.9(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6)(vue@3.4.21): resolution: {integrity: sha512-l80F5sIVdwlQPfw/9RFuhVE1Pi3NM3wbgePxDZkgYZe5XOpg4ZznhgObLRyAFFjCeU7XVbFMBe09uJBRM4tuvg==} dependencies: '@iconify/collections': 1.0.404 '@iconify/vue': 4.1.1(vue@3.4.21) - '@nuxt/devtools-kit': 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6) - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/devtools-kit': 1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6) + '@nuxt/kit': 3.11.0(rollup@3.29.4) transitivePeerDependencies: - nuxt - rollup @@ -6778,8 +6828,8 @@ packages: - vue dev: true - /nuxt@3.10.3(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6): - resolution: {integrity: sha512-NchGNiiz9g/ErJAb462W/lpX2NqcXYb9hugySKWvLXNdrjeAPiJ2/7mhgwUSiZA9MpjuQg3saiEajr1zlRIOCg==} + /nuxt@3.11.0(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6): + resolution: {integrity: sha512-eRjmXk2hC+mUghj46H+sndVjK+VMmS8W5HqWE+k18vASrTxtpHxgG2+gxiiDaOPi/dY0POv+cg5GEs8muZPjEQ==} engines: {node: ^14.18.0 || >=16.10.0} hasBin: true peerDependencies: @@ -6792,15 +6842,15 @@ packages: optional: true dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.0.8(nuxt@3.10.3)(rollup@3.29.4)(vite@5.1.6) - '@nuxt/kit': 3.10.3(rollup@3.29.4) - '@nuxt/schema': 3.10.3(rollup@3.29.4) + '@nuxt/devtools': 1.0.8(nuxt@3.11.0)(rollup@3.29.4)(vite@5.1.6) + '@nuxt/kit': 3.11.0(rollup@3.29.4) + '@nuxt/schema': 3.11.0(rollup@3.29.4) '@nuxt/telemetry': 2.5.3(rollup@3.29.4) '@nuxt/ui-templates': 1.3.1 - '@nuxt/vite-builder': 3.10.3(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vue@3.4.21) - '@unhead/dom': 1.8.19 - '@unhead/ssr': 1.8.19 - '@unhead/vue': 1.8.19(vue@3.4.21) + '@nuxt/vite-builder': 3.11.0(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vue@3.4.21) + '@unhead/dom': 1.8.20 + '@unhead/ssr': 1.8.20 + '@unhead/vue': 1.8.20(vue@3.4.21) '@vue/shared': 3.4.21 acorn: 8.11.3 c12: 1.10.0 @@ -6841,6 +6891,7 @@ packages: unimport: 3.7.1(rollup@3.29.4) unplugin: 1.10.0 unplugin-vue-router: 0.7.0(rollup@3.29.4)(vue-router@4.3.0)(vue@3.4.21) + unstorage: 1.10.2(ioredis@5.3.2) untyped: 1.4.2 vue: 3.4.21(typescript@5.4.2) vue-bundle-renderer: 2.0.0 @@ -6866,6 +6917,7 @@ packages: - encoding - eslint - idb-keyval + - ioredis - less - lightningcss - meow @@ -8072,8 +8124,8 @@ packages: transitivePeerDependencies: - supports-color - /simple-git@3.22.0: - resolution: {integrity: sha512-6JujwSs0ac82jkGjMHiCnTifvf1crOiY/+tfs/Pqih6iow7VrpNKRRNdWm6RtaXpvvv/JGNYhlUtLhGFqHF+Yw==} + /simple-git@3.23.0: + resolution: {integrity: sha512-P9ggTW8vb/21CAL/AmnACAhqBDfnqSSZVpV7WuFtsFR9HLunf5IqQvk+OXAQTfkcZep8pKnt3DV3o7w3TegEkQ==} dependencies: '@kwsites/file-exists': 1.1.1 '@kwsites/promise-deferred': 1.1.1 @@ -8387,8 +8439,8 @@ packages: - supports-color dev: true - /tailwind-merge@2.2.1: - resolution: {integrity: sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==} + /tailwind-merge@2.2.2: + resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} dependencies: '@babel/runtime': 7.24.0 dev: true @@ -8702,12 +8754,12 @@ packages: node-fetch-native: 1.6.2 pathe: 1.1.2 - /unhead@1.8.19: - resolution: {integrity: sha512-H+VfWu27MZi4ZjOnBcPsGudwWlZpljEt9Z740Alq7XpzhNloAf09x1NckoyE4ec0NlEi+WR4kduYWZH6xr2Otw==} + /unhead@1.8.20: + resolution: {integrity: sha512-IJOCYact/7Za3M7CeeCWs8Vze53kHvKDUy/EXtkTm/an5StgqOt2uCnS3HrkioIMKdHBpy/qtTc6E3BoGMOq7Q==} dependencies: - '@unhead/dom': 1.8.19 - '@unhead/schema': 1.8.19 - '@unhead/shared': 1.8.19 + '@unhead/dom': 1.8.20 + '@unhead/schema': 1.8.20 + '@unhead/shared': 1.8.20 hookable: 5.5.3 /unicorn-magic@0.1.0: @@ -9058,7 +9110,7 @@ packages: vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 - /vite-plugin-inspect@0.8.3(@nuxt/kit@3.10.3)(rollup@3.29.4)(vite@5.1.6): + /vite-plugin-inspect@0.8.3(@nuxt/kit@3.11.0)(rollup@3.29.4)(vite@5.1.6): resolution: {integrity: sha512-SBVzOIdP/kwe6hjkt7LSW4D0+REqqe58AumcnCfRNw4Kt3mbS9pEBkch+nupu2PBxv2tQi69EQHQ1ZA1vgB/Og==} engines: {node: '>=14'} peerDependencies: @@ -9069,7 +9121,7 @@ packages: optional: true dependencies: '@antfu/utils': 0.7.7 - '@nuxt/kit': 3.10.3(rollup@3.29.4) + '@nuxt/kit': 3.11.0(rollup@3.29.4) '@rollup/pluginutils': 5.1.0(rollup@3.29.4) debug: 4.3.4 error-stack-parser-es: 0.1.1 @@ -9138,10 +9190,11 @@ packages: /vitest-environment-nuxt@1.0.0(h3@1.11.1)(rollup@3.29.4)(vite@5.1.6)(vitest@1.4.0)(vue-router@4.3.0)(vue@3.4.21): resolution: {integrity: sha512-AWMO9h4HdbaFdPWZw34gALFI8gbBiOpvfbyeZwHIPfh4kWg/TwElYHvYMQ61WPUlCGaS5LebfHkaI0WPyb//Iw==} dependencies: - '@nuxt/test-utils': 3.11.0(h3@1.11.1)(rollup@3.29.4)(vite@5.1.6)(vitest@1.4.0)(vue-router@4.3.0)(vue@3.4.21) + '@nuxt/test-utils': 3.12.0(h3@1.11.1)(rollup@3.29.4)(vite@5.1.6)(vitest@1.4.0)(vue-router@4.3.0)(vue@3.4.21) transitivePeerDependencies: - '@cucumber/cucumber' - '@jest/globals' + - '@playwright/test' - '@testing-library/vue' - '@vitest/ui' - '@vue/test-utils' diff --git a/src/assets.ts b/src/assets.ts new file mode 100644 index 00000000..f3250342 --- /dev/null +++ b/src/assets.ts @@ -0,0 +1,122 @@ +import fsp from 'node:fs/promises' +import { addDevServerHandler, useNuxt } from '@nuxt/kit' +import { createError, eventHandler, lazyEventHandler } from 'h3' +import { fetch } from 'ofetch' +import { colors } from 'consola/utils' +import { defu } from 'defu' +import type { NitroConfig } from 'nitropack' +import { hasProtocol, joinURL, parseURL } from 'ufo' +import { join } from 'pathe' +import { hash } from 'ohash' +import { createStorage } from 'unstorage' +import fsDriver from 'unstorage/drivers/fs-lite' +import { logger } from './logger' + +import type { ModuleOptions } from './types' + +const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365 + +// TODO: replace this with nuxt/assets when it is released +export function setupPublicAssetStrategy(options: ModuleOptions['assets'] = {}) { + const assetsBaseURL = options.prefix || '/_scripts' + const nuxt = useNuxt() + const renderedScriptSrc = new Map() + + // TODO: refactor to use nitro storage when it can be cached between builds + const storage = createStorage(fsDriver({ + base: 'node_modules/.cache/nuxt/scripts', + })) + + function normalizeScriptData(src: string): string { + if (hasProtocol(src, { acceptRelative: true })) { + src = src.replace(/^\/\//, 'https://') + const url = parseURL(src) + const file = [ + `${hash(url)}.js`, // force an extension + ].filter(Boolean).join('-') + + renderedScriptSrc.set(file, src) + return joinURL(assetsBaseURL, file) + } + return src + } + + // Register font proxy URL for development + addDevServerHandler({ + route: assetsBaseURL, + handler: lazyEventHandler(async () => { + return eventHandler(async (event) => { + const filename = event.path.slice(1) + const url = renderedScriptSrc.get(event.path.slice(1)) + if (!url) + throw createError({ statusCode: 404 }) + const key = `data:scripts:${filename}` + // Use storage to cache the font data between requests + let res = await storage.getItemRaw(key) + if (!res) { + res = await fetch(url).then(r => r.arrayBuffer()).then(r => Buffer.from(r)) + await storage.setItemRaw(key, res) + } + return res + }) + }), + }) + + if (nuxt.options.dev) { + nuxt.options.routeRules ||= {} + nuxt.options.routeRules[joinURL(assetsBaseURL, '**')] = { + cache: { + maxAge: ONE_YEAR_IN_SECONDS, + }, + } + } + + nuxt.options.nitro.publicAssets ||= [] + const cacheDir = join(nuxt.options.buildDir, 'cache', 'scripts') + nuxt.options.nitro.publicAssets.push() + nuxt.options.nitro = defu(nuxt.options.nitro, { + publicAssets: [{ + dir: cacheDir, + maxAge: ONE_YEAR_IN_SECONDS, + baseURL: assetsBaseURL, + }], + prerender: { + ignore: [assetsBaseURL], + }, + } satisfies NitroConfig) + + nuxt.hook('nitro:init', async (nitro) => { + if (nuxt.options.dev) + return + nitro.hooks.hook('rollup:before', async () => { + await fsp.rm(cacheDir, { recursive: true, force: true }) + await fsp.mkdir(cacheDir, { recursive: true }) + let banner = false + for (const [filename, url] of renderedScriptSrc) { + const key = `data:scripts:${filename}` + // Use storage to cache the font data between builds + let res = await storage.getItemRaw(key) + if (!res) { + if (!banner) { + banner = true + logger.info('Downloading scripts...') + } + let encoding + let size = 0 + res = await fetch(url).then((r) => { + encoding = r.headers.get('content-encoding') + size = Number(r.headers.get('content-length') / 1024) + return r.arrayBuffer() + }).then(r => Buffer.from(r)) + logger.log(colors.gray(` ├─ ${url} → ${joinURL(assetsBaseURL, filename)} (${size.toFixed(2)} kB ${encoding})`)) + await storage.setItemRaw(key, res) + } + await fsp.writeFile(join(cacheDir, filename), res) + } + if (banner) + logger.success('Scripts downloaded and cached.') + }) + }) + + return { normalizeScriptData } +} diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 00000000..2849a808 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,31 @@ +import { createStorage } from 'unstorage' +import fsDriver from 'unstorage/drivers/fs' + +import type { Awaitable } from './types' + +// TODO: refactor to use nitro storage when possible +const storage = createStorage({ + driver: fsDriver({ + base: 'node_modules/.cache/nuxt/scripts/meta', + }), +}) + +export async function cachedData(key: string, fetcher: () => Awaitable, options?: { + onError?: (err: any) => Awaitable + ttl?: number +}) { + const cached = await storage.getItem(key) + if (!cached || cached.expires < Date.now()) { + try { + const data = await fetcher() + await storage.setItem(key, { expires: Date.now() + (options?.ttl || 1000 * 60 * 60 * 24 * 7), data }) + return data + } + catch (err) { + if (options?.onError) + return options.onError(err) + throw err + } + } + return cached.data +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 00000000..b2829d0c --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,3 @@ +import { useLogger } from '@nuxt/kit' + +export const logger = useLogger('@nuxt/scripts') diff --git a/src/module.ts b/src/module.ts index 459be0d2..a9c9016e 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,8 +1,10 @@ -import { addImportsDir, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' +import { addBuildPlugin, addImportsDir, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' import { readPackageJSON } from 'pkg-types' import type { ScriptBase } from '@unhead/schema' import type { NuxtUseScriptOptions } from './runtime/types' import { setupDevToolsUI } from './devtools' +import { NuxtScriptTransformer } from './plugins/transform' +import { setupPublicAssetStrategy } from './assets' export interface ModuleOptions { /** @@ -18,6 +20,16 @@ export interface ModuleOptions { */ options?: NuxtUseScriptOptions } + /** Configure the way scripts assets are exposed */ + assets?: { + /** + * The baseURL where scripts files are served. + * @default '/_scripts/' + */ + prefix?: string + /** Currently scripts assets are exposed as public assets as part of the build. This will be configurable in future */ + strategy?: 'public' + } /** * Whether the module is enabled. * @@ -68,6 +80,23 @@ export default defineNuxtModule({ nuxt.options.runtimeConfig.public['nuxt-scripts'] = { defaults: config.defaults } addImportsDir(resolve('./runtime/composables')) + const scriptMap = new Map() + const { normalizeScriptData } = setupPublicAssetStrategy(config.assets) + + addBuildPlugin(NuxtScriptTransformer({ + resolveScript(src) { + if (scriptMap.has(src)) + return scriptMap.get(src) as string + const url = normalizeScriptData(src) + scriptMap.set(src, url) + return url + }, + })) + + nuxt.hook('build:manifest', () => { + // TODO ? + }) + if (nuxt.options.dev) setupDevToolsUI(config, resolve) }, diff --git a/src/plugins/transform.ts b/src/plugins/transform.ts new file mode 100644 index 00000000..4a22a48f --- /dev/null +++ b/src/plugins/transform.ts @@ -0,0 +1,101 @@ +import { pathToFileURL } from 'node:url' +import { createUnplugin } from 'unplugin' +import { parseQuery, parseURL } from 'ufo' +import MagicString from 'magic-string' +import type { SourceMapInput } from 'rollup' +import type { Node } from 'estree-walker' +import { walk } from 'estree-walker' +import type { SimpleCallExpression } from 'estree' + +export interface ScriptInjectOptions { + resolveScript: (src: string) => string +} + +/** + * useScript('https://example.com/script.js', { + * assetStrategy: 'bundle', + * }) + * -> + * useScript('') + */ +export function NuxtScriptTransformer(options: ScriptInjectOptions) { + return createUnplugin(() => { + return { + name: 'nuxt:scripts:inline-script-transformer', + enforce: 'post', + + transformInclude(id) { + const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href)) + const { type } = parseQuery(search) + + // vue files + if (pathname.endsWith('.vue') && (type === 'script' || !search)) + return true + + // js files + if (pathname.match(/\.((c|m)?j|t)sx?$/g)) + return true + + if (pathname.includes('node_modules/@unhead') || pathname.includes('node_modules/vueuse')) + return false + + return false + }, + + async transform(code, id) { + if (!code.includes('useScript')) + return + + const ast = this.parse(code) + const s = new MagicString(code) + walk(ast as Node, { + enter(_node) { + if ( + _node.type === 'CallExpression' + && _node.callee.type === 'Identifier' + && _node.callee?.name === 'useScript') { + const node = _node as SimpleCallExpression + if (node.arguments[1].type !== 'ObjectExpression') + return + // second node needs to be an object with an property of assetStrategy and a value of 'bundle' + const assetStrategyProperty = node.arguments[1].properties.find( + (p: any) => p.key?.name === 'assetStrategy' || p.key?.value === 'assetStrategy', + ) + if (assetStrategyProperty?.value?.value !== 'bundle') + return + // remove the property + s.remove(assetStrategyProperty.start, assetStrategyProperty.end + 1) // need to strip the comma + + let scriptNode: any | undefined + // do easy case first where first argument is a literal + if (node.arguments[0].type === 'Literal') { + scriptNode = node.arguments[0] + } + else if (node.arguments[0].type === 'ObjectExpression') { + const srcProperty = node.arguments[0].properties.find( + (p: any) => p.key?.name === 'src' || p.key?.value === 'src', + ) + scriptNode = srcProperty?.value + } + + if (scriptNode) { + const src = scriptNode.value + if (src) { + const newSrc = options.resolveScript(src) + s.overwrite(scriptNode.start, scriptNode.end, `'${newSrc}'`) + } + } + } + }, + }) + + if (s.hasChanged()) { + return { + code: s.toString(), + map: s.generateMap({ includeContent: true, source: id }) as SourceMapInput, + } + } + }, + } + }) +} diff --git a/src/runtime/types.ts b/src/runtime/types.ts index ddd6d819..2cb0fb12 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -2,7 +2,9 @@ import type { UseScriptOptions } from '@unhead/schema' import type { UseScriptInput } from '@unhead/vue' import type { Ref } from 'vue' -export type NuxtUseScriptOptions = UseScriptOptions +export type NuxtUseScriptOptions = UseScriptOptions & { + assetStrategy?: 'bundle' +} export type NuxtUseScriptInput = UseScriptInput diff --git a/vitest.config.ts b/vitest.config.ts index 0563182a..cf471b84 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,14 +2,6 @@ import { defineVitestConfig } from '@nuxt/test-utils/config' export default defineVitestConfig({ test: { - environment: 'nuxt', - setupFiles: ['./test/setup.ts'], - environmentOptions: { - nuxt: { - overrides: { - modules: ['@nuxt/scripts'], - }, - }, - }, + }, })