├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ ├── documentation.yml │ ├── feature-suggestion.yml │ └── help-wanted.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .release-it.json ├── CODE_OF_CONDUCT.md ├── LICENCE ├── README.md ├── build.config.ts ├── eslint.config.js ├── lint-staged.config.cjs ├── package.json ├── playground ├── app.vue ├── nuxt.config.ts ├── package.json ├── pages │ ├── [slug].vue │ └── index.vue └── public │ └── icon.png ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.cjs ├── renovate.json ├── src ├── module.ts └── runtime │ └── extends │ └── app │ └── router.options.ts ├── test └── e2e.test.ts ├── transform.d.ts ├── tsconfig.json └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F41B Bug report" 2 | description: Something's not working 3 | title: "fix: " 4 | labels: ["bug"] 5 | assignees: danielroe 6 | body: 7 | - type: textarea 8 | validations: 9 | required: true 10 | attributes: 11 | label: 🐛 The bug 12 | description: What isn't working? Describe what the bug is. 13 | - type: input 14 | validations: 15 | required: true 16 | attributes: 17 | label: 🛠️ To reproduce 18 | description: A reproduction of the bug via https://stackblitz.com/github/danielroe/nuxt-web-bundle/tree/main/playground 19 | placeholder: https://stackblitz.com/[...] 20 | - type: textarea 21 | validations: 22 | required: true 23 | attributes: 24 | label: 🌈 Expected behaviour 25 | description: What did you expect to happen? Is there a section in the docs about this? 26 | - type: textarea 27 | attributes: 28 | label: ℹ️ Additional context 29 | description: Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Nuxt Community Discord 5 | url: https://discord.nuxtjs.org/ 6 | about: Consider asking questions about the module here. 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F4DA Documentation" 2 | description: How do I ... ? 3 | title: "docs: " 4 | labels: ["documentation"] 5 | assignees: danielroe 6 | body: 7 | - type: textarea 8 | validations: 9 | required: true 10 | attributes: 11 | label: 📚 Is your documentation request related to a problem? 12 | description: A clear and concise description of what the problem is. 13 | placeholder: I feel I should be able to [...] but I can't see how to do it from the docs. 14 | - type: textarea 15 | attributes: 16 | label: 🔍 Where should you find it? 17 | description: What page of the docs do you expect this information to be found on? 18 | - type: textarea 19 | attributes: 20 | label: ℹ️ Additional context 21 | description: Add any other context or information. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-suggestion.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F195 Feature suggestion" 2 | description: Suggest an idea 3 | title: "feat: " 4 | labels: ["enhancement"] 5 | assignees: danielroe 6 | body: 7 | - type: textarea 8 | validations: 9 | required: true 10 | attributes: 11 | label: 🆒 Your use case 12 | description: Add a description of your use case, and how this feature would help you. 13 | placeholder: When I do [...] I would expect to be able to do [...] 14 | - type: textarea 15 | validations: 16 | required: true 17 | attributes: 18 | label: 🆕 The solution you'd like 19 | description: Describe what you want to happen. 20 | - type: textarea 21 | attributes: 22 | label: 🔍 Alternatives you've considered 23 | description: Have you considered any alternative solutions or features? 24 | - type: textarea 25 | attributes: 26 | label: ℹ️ Additional info 27 | description: Is there any other context you think would be helpful to know? 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help-wanted.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F198 Help" 2 | description: I need help with ... 3 | title: "help: " 4 | labels: ["help"] 5 | assignees: danielroe 6 | body: 7 | - type: textarea 8 | validations: 9 | required: true 10 | attributes: 11 | label: 📚 What are you trying to do? 12 | description: A clear and concise description of your objective. 13 | placeholder: I'm not sure how to [...]. 14 | - type: textarea 15 | attributes: 16 | label: 🔍 What have you tried? 17 | description: Have you looked through the docs? Tried different approaches? The more detail the better. 18 | - type: textarea 19 | attributes: 20 | label: ℹ️ Additional context 21 | description: Add any other context or information. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | - renovate/* 11 | - renovate/* 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - run: npm i -g --force corepack && corepack enable 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | cache: "pnpm" 24 | 25 | - name: 📦 Install dependencies 26 | run: pnpm install --frozen-lockfile 27 | 28 | - name: 🚧 Set up project 29 | run: pnpm dev:prepare 30 | 31 | - name: 🔠 Lint project 32 | run: pnpm run lint 33 | 34 | test: 35 | strategy: 36 | matrix: 37 | os: [ubuntu-latest, windows-latest] 38 | runs-on: ${{ matrix.os }} 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - run: npm i -g --force corepack && corepack enable 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version: 18 46 | cache: "pnpm" 47 | 48 | - name: 📦 Install dependencies 49 | run: pnpm install --frozen-lockfile 50 | 51 | - name: 🚧 Set up project 52 | run: pnpm dev:prepare 53 | 54 | - name: 🛠 Build project 55 | run: pnpm build 56 | 57 | - name: 🧪 Test project 58 | run: pnpm test 59 | 60 | - name: 💪 Test types 61 | run: pnpm test:types 62 | 63 | - name: 🟩 Coverage 64 | if: matrix.os != 'windows-latest' 65 | uses: codecov/codecov-action@v5 66 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore: release v${version}" 4 | }, 5 | "github": { 6 | "release": true, 7 | "releaseName": "v${version}" 8 | }, 9 | "npm": { 10 | "skipChecks": true 11 | }, 12 | "plugins": { 13 | "@release-it/conventional-changelog": { 14 | "preset": "conventionalcommits", 15 | "infile": "CHANGELOG.md" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [daniel@roe.dev](mailto:daniel@roe.dev). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 44 | 45 | [homepage]: https://www.contributor-covenant.org 46 | 47 | For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq 48 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Daniel Roe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Web Bundle 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![Github Actions][github-actions-src]][github-actions-href] 6 | [![Codecov][codecov-src]][codecov-href] 7 | 8 | > Generate web bundles with [Nuxt 3](https://v3.nuxtjs.org) 9 | 10 | - [✨  Changelog](https://github.com/danielroe/nuxt-web-bundle/blob/main/CHANGELOG.md) 11 | - [▶️  Online playground](https://stackblitz.com/github/danielroe/nuxt-web-bundle/tree/main/playground) 12 | 13 | ## Features 14 | 15 | **⚠️ `nuxt-web-bundle` is an experiment. ⚠️** 16 | 17 | - 📲 Share your website as a single file over Bluetooth. 18 | - ✨ Run it offline in your origin's context 19 | - ⚡️ Try out experimental web features. 20 | 21 | ## Installation 22 | 23 | With `pnpm` 24 | 25 | ```bash 26 | pnpm add -D nuxt-web-bundle 27 | ``` 28 | 29 | Or, with `npm` 30 | 31 | ```bash 32 | npm install -D nuxt-web-bundle 33 | ``` 34 | 35 | Or, with `yarn` 36 | 37 | ```bash 38 | yarn add -D nuxt-web-bundle 39 | ``` 40 | 41 | ## Usage 42 | 43 | ```js 44 | export default defineNuxtConfig({ 45 | modules: ['nuxt-web-bundle'], 46 | webBundle: { 47 | baseURL: 'https://my-website.com', 48 | // filename: 'bundle.wbn', 49 | }, 50 | }) 51 | ``` 52 | 53 | That's it! Now when you run `nuxi build` or `nuxi generate`, a web bundle will be generated _instead_ of a server or static directory. 54 | 55 | As mentioned earlier, this is an experiment, and in order to experiment with Web Bundles, you can follow [the steps here](https://web.dev/web-bundles/#playing-around-with-web-bundles) to enable usage in your version of Google Chrome. 56 | 57 | ## 💻 Development 58 | 59 | - Clone this repository 60 | - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` 61 | - Install dependencies using `pnpm install` 62 | - Stub module with `pnpm dev:prepare` 63 | - Run `pnpm dev` to start [playground](./playground) in development mode 64 | 65 | ## Credits 66 | 67 | Much of the implementation is taken from [rollup-plugin-webbundle](https://github.com/GoogleChromeLabs/rollup-plugin-webbundle) - check it out and try it if you are using Vite or another Rollup-based build system. 68 | 69 | ## License 70 | 71 | Made with ❤️ 72 | 73 | Published under the [MIT License](./LICENCE). 74 | 75 | 76 | 77 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-web-bundle?style=flat-square 78 | [npm-version-href]: https://npmjs.com/package/nuxt-web-bundle 79 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-web-bundle?style=flat-square 80 | [npm-downloads-href]: https://npm.chart.dev/nuxt-web-bundle 81 | [github-actions-src]: https://img.shields.io/github/actions/workflow/status/danielroe/nuxt-web-bundle/ci.yml?branch=main&style=flat-square 82 | [github-actions-href]: https://github.com/danielroe/nuxt-web-bundle/actions/workflows/ci.yml 83 | [codecov-src]: https://img.shields.io/codecov/c/gh/danielroe/nuxt-web-bundle/main?style=flat-square 84 | [codecov-href]: https://codecov.io/gh/danielroe/nuxt-web-bundle 85 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | rollup: { emitCJS: true }, 5 | // TODO: fix in unbuild 6 | externals: ['node:url', 'node:path', 'node:fs/promises'], 7 | }) 8 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 3 | 4 | export default createConfigForNuxt({ 5 | features: { 6 | tooling: true, 7 | stylistic: true, 8 | }, 9 | dirs: { 10 | src: [ 11 | './playground', 12 | ], 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /lint-staged.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,ts,mjs,cjs,json}': ['pnpm lint:eslint'], 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-web-bundle", 3 | "version": "0.3.0", 4 | "license": "MIT", 5 | "description": "Generate web bundles with Nuxt", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/danielroe/nuxt-web-bundle.git" 9 | }, 10 | "keywords": [ 11 | "nuxt", 12 | "module", 13 | "nuxt-module", 14 | "web-bundle", 15 | "experiment", 16 | "performance" 17 | ], 18 | "author": { 19 | "name": "Daniel Roe ", 20 | "url": "https://github.com/danielroe" 21 | }, 22 | "type": "module", 23 | "exports": { 24 | ".": { 25 | "types": "./dist/types.d.mts", 26 | "import": "./dist/module.mjs" 27 | } 28 | }, 29 | "main": "./dist/module.mjs", 30 | "typesVersions": { 31 | "*": { 32 | ".": [ 33 | "./dist/types.d.mts" 34 | ] 35 | } 36 | }, 37 | "files": [ 38 | "dist" 39 | ], 40 | "scripts": { 41 | "build": "pnpm dev:prepare && nuxt-module-build build", 42 | "dev": "nuxt dev playground", 43 | "dev:build": "nuxt build playground", 44 | "dev:prepare": "pnpm nuxt-module-build build --stub && pnpm nuxt-module-build prepare && nuxt prepare playground", 45 | "docs:dev": "nuxt dev docs", 46 | "docs:build": "nuxt generate docs", 47 | "lint": "pnpm lint:all:eslint", 48 | "lint:all:eslint": "pnpm lint:eslint .", 49 | "lint:eslint": "eslint --fix", 50 | "prepack": "pnpm build", 51 | "prepare": "husky install", 52 | "prepublishOnly": "pnpm lint && pnpm test", 53 | "release": "bumpp && pnpm publish", 54 | "test": "vitest run --coverage", 55 | "test:types": "tsc --noEmit" 56 | }, 57 | "dependencies": { 58 | "@nuxt/kit": "^3.11.2", 59 | "chalk": "^5.3.0", 60 | "globby": "^14.0.1", 61 | "mime": "^4.0.1", 62 | "pathe": "^2.0.0", 63 | "ufo": "^1.5.3", 64 | "wbn": "^0.0.9" 65 | }, 66 | "devDependencies": { 67 | "@nuxt/eslint-config": "1.0.1", 68 | "@nuxt/module-builder": "1.0.1", 69 | "@nuxt/schema": "3.17.5", 70 | "@nuxt/test-utils": "3.19.1", 71 | "@types/mime": "4.0.0", 72 | "@types/node": "22.9.0", 73 | "@vitest/coverage-v8": "3.2.0", 74 | "bumpp": "10.0.1", 75 | "eslint": "9.28.0", 76 | "expect-type": "1.1.0", 77 | "get-port-please": "3.1.2", 78 | "husky": "9.1.6", 79 | "lint-staged": "16.0.0", 80 | "nuxt": "3.17.5", 81 | "typescript": "5.6.3", 82 | "vitest": "3.2.0", 83 | "vue": "3.5.16" 84 | }, 85 | "resolutions": { 86 | "@nuxt/kit": "3.17.5", 87 | "nuxt-web-bundle": "link:." 88 | }, 89 | "packageManager": "pnpm@10.2.1" 90 | } 91 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | compatibilityDate: '2024-08-19', 3 | modules: ['nuxt-web-bundle'], 4 | }) 5 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nuxt-web-bundle-playground", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "nuxi build && echo 'Download bundle.wbn and open locally to test.' && npx serve dist" 7 | }, 8 | "devDependencies": { 9 | "nuxt": "latest", 10 | "nuxt-web-bundle": "latest" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /playground/pages/[slug].vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /playground/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielroe/nuxt-web-bundle/a3c189ce9f47c10d397263b204b8b29cf034eb74/playground/public/icon.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "playground" 3 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | printWidth: 100, 5 | trailingComma: 'es5', 6 | arrowParens: 'avoid', 7 | } 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>danielroe/renovate" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import fsp from 'node:fs/promises' 2 | import { fileURLToPath, pathToFileURL } from 'node:url' 3 | 4 | import { defineNuxtModule, useLogger } from '@nuxt/kit' 5 | import chalk from 'chalk' 6 | import { globby } from 'globby' 7 | import { basename, dirname, resolve } from 'pathe' 8 | import { joinURL, parseURL } from 'ufo' 9 | import { BundleBuilder } from 'wbn' 10 | import mime from 'mime' 11 | 12 | export interface ModuleOptions { 13 | baseURL: string 14 | formatVersion: string 15 | primaryURL?: string 16 | filename: string 17 | } 18 | 19 | export default defineNuxtModule({ 20 | meta: { 21 | configKey: 'webBundle', 22 | name: 'nuxt-web-bundle', 23 | }, 24 | defaults: nuxt => ({ 25 | baseURL: joinURL('http://localhost:3000', nuxt.options.app.baseURL), 26 | formatVersion: 'b2', 27 | filename: 'bundle.wbn', 28 | }), 29 | async setup(options, nuxt) { 30 | // Skip when preparing 31 | if (nuxt.options._prepare) return 32 | 33 | nuxt.options._generate = true 34 | 35 | nuxt.hook('nitro:config', (config) => { 36 | config.prerender ||= {} 37 | config.prerender.crawlLinks = true 38 | config.prerender.routes = [ 39 | ...(config.prerender.routes || []), 40 | nuxt.options.ssr ? '/' : '/index.html', 41 | ].filter(i => i !== '/200.html') 42 | }) 43 | 44 | const themeDir = fileURLToPath(new URL('./runtime/extends', import.meta.url)) 45 | nuxt.options._layers.push({ 46 | cwd: themeDir, 47 | config: { rootDir: themeDir, srcDir: themeDir }, 48 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 49 | } as any) 50 | 51 | const logger = useLogger('nuxt-web-bundle') 52 | 53 | nuxt.hook('ready', () => { 54 | nuxt.hook('build:done', async () => { 55 | logger.info('Generating web bundle.') 56 | 57 | // Credit: https://github.com/GoogleChromeLabs/rollup-plugin-webbundle 58 | const builder = new BundleBuilder('b2') 59 | builder.setPrimaryURL(options.primaryURL || options.baseURL) 60 | 61 | const files = await globby('**/*', { 62 | cwd: resolve(nuxt.options.rootDir, 'dist'), 63 | }) 64 | 65 | for (const [index, file] of files.sort().reverse().entries()) { 66 | const content = await fsp.readFile(resolve(nuxt.options.rootDir, 'dist', file)) 67 | const headers = { 68 | 'Content-Type': mime.getType(basename(file)) || 'application/octet-stream', 69 | 'Access-Control-Allow-Origin': '*', 70 | } 71 | const url = new URL(file, options.baseURL) 72 | .toString() 73 | .replace(/\/index.html$/, '/') 74 | .replace(/\.html$/, '') 75 | builder.addExchange(url, 200, headers, content) 76 | 77 | let dir = dirname(url) 78 | if (dir === '.') { 79 | dir = '' 80 | } 81 | const { pathname } = parseURL(url) 82 | const treeChar = index === files.length - 1 ? '└─' : '├─' 83 | 84 | process.stdout.write(chalk.gray(` ${treeChar} ${pathname}\n`)) 85 | await fsp.rm(resolve(nuxt.options.rootDir, 'dist', file), { force: true }) 86 | } 87 | 88 | const buf = builder.createBundle() 89 | await fsp.mkdir(resolve(nuxt.options.rootDir, 'dist'), { recursive: true }) 90 | await fsp.writeFile( 91 | resolve(nuxt.options.rootDir, 'dist', options.filename), 92 | Buffer.from(buf, buf.byteOffset, buf.byteLength), 93 | ) 94 | const url = pathToFileURL(resolve(nuxt.options.rootDir, 'dist', options.filename)) 95 | logger.success(`Bundle generated at \`${url}\`.`) 96 | }) 97 | }) 98 | }, 99 | }) 100 | -------------------------------------------------------------------------------- /src/runtime/extends/app/router.options.ts: -------------------------------------------------------------------------------- 1 | import type { RouterOptions } from '@nuxt/schema' 2 | import { createMemoryHistory } from 'vue-router' 3 | 4 | export default { 5 | history: (baseURL) => { 6 | return createMemoryHistory(baseURL) 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /test/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import fsp from 'node:fs/promises' 2 | import { fileURLToPath } from 'node:url' 3 | import { describe, it, expect } from 'vitest' 4 | import { setup } from '@nuxt/test-utils' 5 | import { resolve } from 'pathe' 6 | 7 | const rootDir = fileURLToPath(new URL('../playground', import.meta.url)) 8 | 9 | describe('builds a web bundle', async () => { 10 | await setup({ 11 | build: true, 12 | server: false, 13 | rootDir, 14 | }) 15 | it('inlines rules', async () => { 16 | const publicDir = resolve(rootDir, 'dist') 17 | const files = await fsp.readdir(publicDir) 18 | expect(files).toContain('bundle.wbn') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /transform.d.ts: -------------------------------------------------------------------------------- 1 | // Legacy stub for previous TS versions 2 | 3 | export * from './dist/transform' 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | } 4 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ['text', 'json'], 7 | }, 8 | }, 9 | }) 10 | --------------------------------------------------------------------------------