├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── client.d.ts ├── eslint.config.js ├── package.json ├── playground ├── app.ts ├── env.d.ts ├── index.html ├── package.json ├── pages │ ├── Page1.vue │ └── Page2.vue ├── test_node_modules │ ├── my-plugin1 │ │ ├── package.json │ │ └── src │ │ │ └── Pages │ │ │ └── Page3.vue │ └── my-plugin2 │ │ ├── package.json │ │ └── src │ │ └── other-pages │ │ ├── Page222.vue │ │ └── Page223.vue ├── test_vendor │ └── ycs77 │ │ └── my-php-package │ │ ├── composer.json │ │ └── resources │ │ └── js │ │ └── Pages │ │ └── PhpPackagePage.vue └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── postbuild.ts ├── src ├── farm.ts ├── index.ts ├── module │ ├── files.ts │ └── import.ts ├── page │ ├── generate-namespaces.ts │ ├── index.ts │ └── namespace-option.ts ├── rollup.ts ├── rspack.ts ├── runtime │ ├── index.ts │ └── plugin.ts ├── types.ts ├── utils.ts ├── vite.ts └── webpack.ts ├── tests ├── generate-namespaces.test.ts ├── namespace-option.test.ts ├── test_node_modules │ ├── my-plugin1 │ │ ├── package.json │ │ └── src │ │ │ └── Pages │ │ │ └── Page3.vue │ └── my-plugin2 │ │ ├── package.json │ │ └── src │ │ └── other-pages │ │ ├── Page222.vue │ │ └── Page223.vue └── test_vendor │ └── ycs77 │ └── my-php-package │ ├── composer.json │ └── resources │ └── js │ └── Pages │ └── PhpPackagePage.vue ├── tsconfig.json ├── tsup.config.ts └── tsup.runtime-config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [composer.json] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: ycs77 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Use Version:** 11 | Use version when bugs appear. 12 | - vite: v5.0.0 / laravel-mix v6.0.0 13 | - inertia-page-loader: v0.6.0 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | Please provide a minimal working example (like github repo, codesandbox, stackblitz...), and steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Expected behavior** 29 | A clear and concise description of what you expected to happen. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: pnpm/action-setup@v4 19 | name: Install pnpm 20 | with: 21 | run_install: false 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: Lint 33 | run: pnpm lint 34 | 35 | test: 36 | runs-on: ubuntu-latest 37 | 38 | strategy: 39 | matrix: 40 | node: [20.x] 41 | os: [ubuntu-latest, windows-latest, macos-latest] 42 | fail-fast: false 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - uses: pnpm/action-setup@v4 48 | name: Install pnpm 49 | with: 50 | run_install: false 51 | 52 | - name: Setup Node.js ${{ matrix.node }} 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: ${{ matrix.node }} 56 | cache: pnpm 57 | 58 | - name: Install dependencies 59 | run: pnpm install 60 | 61 | - name: Build 62 | run: pnpm build 63 | 64 | - name: Tests 65 | run: pnpm test 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | /*.d.ts 83 | !/client.d.ts 84 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit", 6 | "source.organizeImports": "never" 7 | }, 8 | "eslint.rules.customizations": [ 9 | { "rule": "style/*", "severity": "off" }, 10 | { "rule": "format/*", "severity": "off" }, 11 | { "rule": "*-indent", "severity": "off" }, 12 | { "rule": "*-spacing", "severity": "off" }, 13 | { "rule": "*-spaces", "severity": "off" }, 14 | { "rule": "*-order", "severity": "off" }, 15 | { "rule": "*-dangle", "severity": "off" }, 16 | { "rule": "*-newline", "severity": "off" }, 17 | { "rule": "*quotes", "severity": "off" }, 18 | { "rule": "*semi", "severity": "off" } 19 | ], 20 | "eslint.validate": [ 21 | "javascript", 22 | "javascriptreact", 23 | "typescript", 24 | "typescriptreact", 25 | "vue", 26 | "html", 27 | "markdown", 28 | "json", 29 | "jsonc", 30 | "yaml", 31 | "toml" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present Lucas Yang 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 | # Inertia Page Loader 2 | 3 | [![NPM version][ico-version]][link-npm] 4 | [![Software License][ico-license]](LICENSE) 5 | [![GitHub Tests Action Status][ico-github-action]][link-github-action] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | 8 | The plugin page loader for Inertia.js, that allows the server-side to use `Inertia::render('my-package::Page');`. 9 | 10 | ## Features 11 | 12 | * Powered by [unplugin](https://github.com/unjs/unplugin) 13 | * Supports **static** build with [Vite](https://vitejs.dev/) and [Laravel Mix](https://laravel-mix.com/) 14 | * Supports load pages on **runtime** 15 | * Define the namespace mapping for plugins **pages** directory 16 | * Or read namespace from the **npm** / **composer** package 17 | 18 | ## Install 19 | 20 | First, install the Inertia Plugin to your main Inertia app: 21 | 22 | ```bash 23 | npm i inertia-page-loader -D 24 | ``` 25 | 26 |
27 | Vite
28 | 29 | ```js 30 | // vite.config.js 31 | import InertiaPageLoader from 'inertia-page-loader/vite' 32 | 33 | export default defineConfig({ 34 | plugins: [ 35 | InertiaPageLoader({ /* options */ }), 36 | ], 37 | }) 38 | ``` 39 | 40 |
41 | 42 |
43 | Webpack
44 | 45 | ```js 46 | // webpack.config.js 47 | const InertiaPageLoaderPlugin = require('inertia-page-loader/webpack') 48 | 49 | module.exports = { 50 | /* ... */ 51 | plugins: [ 52 | InertiaPageLoaderPlugin({ /* options */ }), 53 | ], 54 | } 55 | ``` 56 | 57 |
58 | 59 |
60 | Laravel Mix
61 | 62 | ```js 63 | // webpack.mix.js 64 | const InertiaPageLoaderPlugin = require('inertia-page-loader/webpack') 65 | 66 | mix 67 | .webpackConfig({ 68 | plugins: [ 69 | InertiaPageLoaderPlugin({ /* options */ }), 70 | ], 71 | }) 72 | ``` 73 | 74 |
75 | 76 | ### Type 77 | 78 | Add to `env.d.ts`: 79 | 80 | ```ts 81 | /// 82 | ``` 83 | 84 | ## Usage 85 | 86 | This package supports the **Static** and **Runtime** to load the pages (can be mixed to use), so you can select the way to build and use your Inertia pages: 87 | 88 | 89 | * [Build for Static](#build-for-static) 90 | * [Build for Runtime](#build-for-runtime) 91 | 92 | ## Build for Static 93 | 94 | Then select the source from which you want to load the page: 95 | 96 | 97 | * [NPM Package](#load-pages-from-npm-package) 98 | * [Composer Package](#load-pages-from-composer-package) 99 | * [Modules (in the main app)](#load-pages-from-modules-in-main-app) 100 | 101 | If you created or have a package, you can select the build tool to use the package: 102 | 103 | 104 | * [Usage with Vite](#usage-with-vite) 105 | * [Usage with Laravel Mix](#usage-with-laravel-mix) 106 | 107 | ### Load Pages from NPM Package 108 | 109 | You must create an npm package that contains the `pages` folder: 110 | 111 | ``` 112 | src/pages/ 113 | ├── Some.vue 114 | └── Dir/ 115 | └── Other.vue 116 | ``` 117 | 118 | And added the `inertia` field to define the namespace mapping, for example in `node_modules/my-plugin/package.json`: 119 | 120 | ```json 121 | { 122 | "name": "my-plugin", 123 | "inertia": { 124 | "my-package": "src/pages" 125 | } 126 | } 127 | ``` 128 | 129 | Publish this package and back to the main Inertia app to install this package: 130 | 131 | ```bash 132 | npm i my-plugin 133 | ``` 134 | 135 | Next step you can select the build tool to use: 136 | 137 | 138 | * [Usage with Vite](#usage-with-vite) 139 | * [Usage with Laravel Mix](#usage-with-laravel-mix) 140 | 141 | ### Load Pages from Composer Package 142 | 143 | You must create a composer package that contains the `pages` folder: 144 | 145 | ``` 146 | resources/js/pages/ 147 | ├── Some.vue 148 | └── Dir/ 149 | └── Other.vue 150 | ``` 151 | 152 | And added the `extra.inertia` field to define the namespace mapping, for example in `vendor/ycs77/my-php-package/composer.json`: 153 | 154 | ```json 155 | { 156 | "name": "ycs77/my-php-package", 157 | "extra": { 158 | "inertia": { 159 | "my-php-package": "resources/js/pages" 160 | } 161 | } 162 | } 163 | ``` 164 | 165 | Publish this package and back to the main Inertia app to install this package: 166 | 167 | ```bash 168 | composer require ycs77/my-php-package 169 | ``` 170 | 171 | Next step you can select the build tool to use: 172 | 173 | 174 | * [Usage with Vite](#usage-with-vite) 175 | * [Usage with Laravel Mix](#usage-with-laravel-mix) 176 | 177 | ### Usage with Vite 178 | 179 | Add `inertia-page-loader` to `vite.config.js`, and you can use the function `npm()` or `composer()` to load the namespace: 180 | 181 | ```js 182 | import InertiaPageLoader from 'inertia-page-loader/vite' 183 | 184 | export default defineConfig({ 185 | plugins: [ 186 | InertiaPageLoader({ 187 | namespaces: ({ npm, composer }) => [ 188 | // load namespace from npm package: 189 | npm('my-plugin'), 190 | 191 | // load namespace from composer package: 192 | composer('ycs77/my-php-package'), 193 | ], 194 | }), 195 | ], 196 | }) 197 | ``` 198 | 199 | And use `resolvePage()` in `resources/js/app.js` to resolve the app pages and npm / composer pages (**don't use one line function**): 200 | 201 | ```js 202 | import { resolvePage } from '~inertia' 203 | 204 | createInertiaApp({ 205 | resolve: resolvePage(() => { 206 | return import.meta.glob('./pages/**/*.vue', { eager: true }) 207 | }), 208 | }) 209 | ``` 210 | 211 | Or you can add the persistent layout: 212 | 213 | ```js 214 | import Layout from './Layout' 215 | 216 | createInertiaApp({ 217 | resolve: resolvePage(name => { 218 | return import.meta.glob('./pages/**/*.vue', { eager: true }) 219 | }, page => { 220 | page.layout = Layout 221 | return page 222 | }), 223 | }) 224 | ``` 225 | 226 | Now you can use the page in your controller: 227 | 228 | ```php 229 | Inertia::render('my-package::Some'); // in npm package 230 | Inertia::render('my-php-package::Some'); // in composer package 231 | ``` 232 | 233 | ### Usage with Laravel Mix 234 | 235 | Add `inertia-page-loader` to `webpack.mix.js`, and you can use the function `npm()` or `composer()` to load the namespace: 236 | 237 | ```js 238 | mix 239 | .webpackConfig({ 240 | plugins: [ 241 | InertiaPageLoaderPlugin({ 242 | namespaces: ({ npm, composer }) => [ 243 | // load namespace from npm package: 244 | npm('my-plugin'), 245 | 246 | // load namespace from composer package: 247 | composer('ycs77/my-php-package'), 248 | ], 249 | }), 250 | ], 251 | }) 252 | ``` 253 | 254 | And use `resolvePage()` in `resources/js/app.js` to resolve the app pages and npm / composer pages: 255 | 256 | ```js 257 | import { resolvePage } from '~inertia' 258 | 259 | createInertiaApp({ 260 | resolve: resolvePage(name => require(`./pages/${name}`)), 261 | }) 262 | ``` 263 | 264 | Or you can add the persistent layout: 265 | 266 | ```js 267 | import Layout from './Layout' 268 | 269 | createInertiaApp({ 270 | resolve: resolvePage(name => require(`./pages/${name}`), page => { 271 | page.layout = Layout 272 | return page 273 | }), 274 | }) 275 | ``` 276 | 277 | Now you can use the page in your controller: 278 | 279 | ```php 280 | Inertia::render('my-package::Some'); // in npm package 281 | Inertia::render('my-php-package::Some'); // in composer package 282 | ``` 283 | 284 | ### Load pages from Modules (in the main app) 285 | 286 | If you use the modules package to manage your Laravel application, such as [Laravel Modules](https://github.com/nWidart/laravel-modules), you can also define namespace mapping: 287 | 288 | > **Note**: Of course, can also be load pages from other locations in the main application. 289 | 290 | ```js 291 | export default defineConfig({ 292 | plugins: [ 293 | InertiaPageLoader({ 294 | namespaces: [ 295 | // define namespace mapping: 296 | { 'my-module': 'Modules/MyModule/Resources/js/pages' }, 297 | 298 | // define more namespace mapping: 299 | { 300 | 'my-module-2': 'Modules/MyModule2/Resources/js/pages', 301 | 'special-modal': 'resources/js/SpecialModals', 302 | }, 303 | ], 304 | }), 305 | ], 306 | }) 307 | ``` 308 | 309 | Now you can use the page in your controller: 310 | 311 | ```php 312 | Inertia::render('my-module::Some'); 313 | Inertia::render('my-module-2::Some'); 314 | Inertia::render('special-modal::VeryCoolModal'); 315 | ``` 316 | 317 | ## Build for Runtime 318 | 319 | > [!WARNING] 320 | > The runtime is not working with Vue Composition, it's recommended to use [Build for Static](#build-for-static). 321 | 322 | Sometimes you may want users to use the pages without compiling them after installing the composer package, at this time you can load them at runtime. This is the package directory structure: 323 | 324 | ``` 325 | resources/js/ 326 | ├── my-runtime-plugin.js 327 | └── pages/ 328 | ├── Some.vue 329 | └── Other.vue 330 | ``` 331 | 332 | Use the **InertiaPages** runtime API in `resources/js/my-runtime-plugin.js` to load pages: 333 | 334 | ```js 335 | window.InertiaPages.addNamespace('my-runtime', name => require(`./pages/${name}`)) 336 | ``` 337 | 338 | And setting `webpack.mix.js` to build assets: 339 | 340 | ```js 341 | const mix = require('laravel-mix') 342 | 343 | mix 344 | .setPublicPath('public') 345 | .js('resources/js/my-runtime-plugin.js', 'public/js') 346 | .vue({ runtimeOnly: true }) 347 | .version() 348 | .disableNotifications() 349 | ``` 350 | 351 | Now you can publish this package and install it in the Inertia app, publish assets (`my-runtime-plugin.js`) to `public/vendor/inertia-plugins`, and open `app.blade.php` to include scripts to load pages: 352 | 353 | ```html 354 | 355 | 356 | 357 | 358 | 359 | 360 | ``` 361 | 362 | But the `app.js` must build with `inertia-page-loader`, you can follow [Install](#install) chapter to install it (does not need to include any option), like this: 363 | 364 | ```js 365 | // vite.config.js 366 | import InertiaPageLoader from 'inertia-page-loader/vite' 367 | 368 | export default defineConfig({ 369 | plugins: [ 370 | InertiaPageLoader(), 371 | ], 372 | }) 373 | ``` 374 | 375 | Or using in Laravel Mix: 376 | 377 | ```js 378 | // webpack.mix.js 379 | const InertiaPageLoaderPlugin = require('inertia-page-loader/webpack') 380 | 381 | mix 382 | .webpackConfig({ 383 | plugins: [ 384 | InertiaPageLoaderPlugin(), 385 | ], 386 | }) 387 | ``` 388 | 389 | Now you can use the page in your controller: 390 | 391 | ```php 392 | Inertia::render('my-runtime::Some'); 393 | ``` 394 | 395 | ## Migrate from `inertia-plugin` 396 | 397 | Update package in `package.json`: 398 | 399 | ```diff 400 | { 401 | "devDependencies": { 402 | - "inertia-plugin": "*" 403 | + "inertia-page-loader": "^0.7.0" 404 | } 405 | } 406 | ``` 407 | 408 | Rename vite plugin: 409 | 410 | ```diff 411 | // vite.config.js 412 | -import Inertia from 'inertia-plugin/vite' 413 | +import InertiaPageLoader from 'inertia-page-loader/vite' 414 | 415 | export default defineConfig({ 416 | plugins: [ 417 | - Inertia({ /* options */ }), 418 | + InertiaPageLoader({ /* options */ }), 419 | ], 420 | }) 421 | ``` 422 | 423 | Rename webpack plugin: 424 | 425 | ```diff 426 | // webpack.config.js 427 | -const InertiaPlugin = require('inertia-plugin/webpack') 428 | +const InertiaPageLoaderPlugin = require('inertia-page-loader/webpack') 429 | 430 | module.exports = { 431 | /* ... */ 432 | plugins: [ 433 | - InertiaPlugin({ /* options */ }), 434 | + InertiaPageLoaderPlugin({ /* options */ }), 435 | ], 436 | } 437 | ``` 438 | 439 | Update CDN link if you used: 440 | 441 | ```diff 442 | - 443 | + 444 | ``` 445 | 446 | ## Configuration 447 | 448 | ```js 449 | InertiaPageLoader({ 450 | // Current work directory. 451 | cwd: process.cwd(), 452 | 453 | // Define namespace mapping. 454 | namespaces: [], 455 | 456 | // Namespace separator. 457 | separator: '::', 458 | 459 | // Module extensions. 460 | extensions: '', 461 | // extensions: '', // webpack default 462 | // extensions: 'vue', // webpack example 463 | // extensions: 'vue', // vite default 464 | // extensions: ['vue', 'js'], // vite example 465 | 466 | // Use `import()` to load pages for webpack, default is using `require()`. 467 | // Only for webpack. 468 | import: false, 469 | 470 | // Enable SSR mode. 471 | ssr: false, 472 | }) 473 | ``` 474 | 475 | ## Sponsor 476 | 477 | If you think this package has helped you, please consider [Becoming a sponsor](https://www.patreon.com/ycs77) to support my work~ and your avatar will be visible on my major projects. 478 | 479 |

480 | 481 | 482 | 483 |

484 | 485 | 486 | Become a Patron 487 | 488 | 489 | ## Credits 490 | 491 | * [inertia-laravel#92](https://github.com/inertiajs/inertia-laravel/issues/92) 492 | * [unplugin](https://github.com/unjs/unplugin) 493 | * [Laravel](https://laravel.com/) 494 | 495 | ## License 496 | 497 | [MIT LICENSE](LICENSE) 498 | 499 | [ico-version]: https://img.shields.io/npm/v/inertia-page-loader?style=flat-square 500 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen?style=flat-square 501 | [ico-github-action]: https://img.shields.io/github/actions/workflow/status/ycs77/inertia-page-loader/ci.yml?branch=main&label=tests&style=flat-square 502 | [ico-downloads]: https://img.shields.io/npm/dt/inertia-page-loader?style=flat-square 503 | 504 | [link-npm]: https://www.npmjs.com/package/inertia-page-loader 505 | [link-github-action]: https://github.com/ycs77/inertia-page-loader/actions/workflows/ci.yml?query=branch%3Amain 506 | [link-downloads]: https://www.npmjs.com/package/inertia-page-loader 507 | -------------------------------------------------------------------------------- /client.d.ts: -------------------------------------------------------------------------------- 1 | declare module '~inertia' { 2 | function resolvePage(resolver: (name: string) => any | Promise, transformPage?: (page: T, name: string) => T): (name: string) => any 3 | function resolvePluginPage(name: string): Promise 4 | function resolveVitePage(name: string, pages: Record, throwNotFoundError?: boolean): T 5 | } 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import ycs77 from '@ycs77/eslint-config' 2 | 3 | export default ycs77({ 4 | typescript: true, 5 | ignores: [ 6 | '**/composer.json', 7 | '**/*.md', 8 | ], 9 | }) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inertia-page-loader", 3 | "type": "module", 4 | "version": "0.8.0", 5 | "packageManager": "pnpm@9.9.0", 6 | "description": "The plugin page loader for Inertia.js", 7 | "author": "Lucas Yang ", 8 | "license": "MIT", 9 | "homepage": "https://github.com/ycs77/inertia-page-loader#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ycs77/inertia-page-loader.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/ycs77/inertia-page-loader/issues" 16 | }, 17 | "keywords": [ 18 | "inertia", 19 | "inertiajs", 20 | "unplugin", 21 | "vite", 22 | "webpack", 23 | "rollup", 24 | "transform" 25 | ], 26 | "sideEffects": false, 27 | "exports": { 28 | ".": { 29 | "import": "./dist/index.mjs", 30 | "require": "./dist/index.cjs" 31 | }, 32 | "./rspack": { 33 | "import": "./dist/rspack.mjs", 34 | "require": "./dist/rspack.cjs" 35 | }, 36 | "./vite": { 37 | "import": "./dist/vite.mjs", 38 | "require": "./dist/vite.cjs" 39 | }, 40 | "./webpack": { 41 | "import": "./dist/webpack.mjs", 42 | "require": "./dist/webpack.cjs" 43 | }, 44 | "./rollup": { 45 | "import": "./dist/rollup.mjs", 46 | "require": "./dist/rollup.cjs" 47 | }, 48 | "./farm": { 49 | "import": "./dist/farm.mjs", 50 | "require": "./dist/farm.cjs" 51 | }, 52 | "./runtime": { 53 | "import": "./dist/runtime.mjs", 54 | "require": "./dist/runtime.cjs" 55 | }, 56 | "./types": { 57 | "import": "./dist/types.mjs", 58 | "require": "./dist/types.cjs" 59 | }, 60 | "./*": "./*" 61 | }, 62 | "main": "dist/index.cjs", 63 | "module": "dist/index.mjs", 64 | "unpkg": "dist/runtime.iife.js", 65 | "jsdelivr": "dist/runtime.iife.js", 66 | "types": "dist/index.d.ts", 67 | "typesVersions": { 68 | "*": { 69 | "*": [ 70 | "./dist/*", 71 | "./*" 72 | ] 73 | } 74 | }, 75 | "files": [ 76 | "*.d.ts", 77 | "dist" 78 | ], 79 | "scripts": { 80 | "build": "run-s build:plugin build:runtime", 81 | "build:plugin": "tsup", 82 | "build:runtime": "tsup --config tsup.runtime-config.ts", 83 | "build:fix": "tsx scripts/postbuild.ts", 84 | "dev": "tsup --watch src", 85 | "lint": "eslint .", 86 | "play": "npm -C playground run dev", 87 | "prepublishOnly": "npm run build", 88 | "release": "bumpp --commit \"Release v%s\" && pnpm publish", 89 | "test": "vitest" 90 | }, 91 | "peerDependencies": { 92 | "@farmfe/core": ">=1", 93 | "@inertiajs/core": ">=1", 94 | "rollup": "^3", 95 | "vite": ">=3", 96 | "webpack": "^4 || ^5" 97 | }, 98 | "peerDependenciesMeta": { 99 | "@farmfe/core": { 100 | "optional": true 101 | }, 102 | "@inertiajs/core": { 103 | "optional": true 104 | }, 105 | "rollup": { 106 | "optional": true 107 | }, 108 | "vite": { 109 | "optional": true 110 | }, 111 | "webpack": { 112 | "optional": true 113 | } 114 | }, 115 | "dependencies": { 116 | "debug": "^4.3.6", 117 | "fast-glob": "^3.3.2", 118 | "unplugin": "^1.13.1" 119 | }, 120 | "devDependencies": { 121 | "@swc/core": "^1.7.23", 122 | "@types/debug": "^4.1.12", 123 | "@types/node": "^18.19.50", 124 | "@ycs77/eslint-config": "^3.0.1", 125 | "bumpp": "^9.5.2", 126 | "eslint": "^9.9.1", 127 | "npm-run-all2": "^6.2.2", 128 | "rollup": "^4.21.2", 129 | "tsup": "^8.2.4", 130 | "tsx": "^4.19.0", 131 | "typescript": "~5.5.4", 132 | "vite": "^5.4.3", 133 | "vitest": "^2.0.5", 134 | "webpack": "^5.94.0" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /playground/app.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from 'vue' 2 | import { createInertiaApp } from '@inertiajs/vue3' 3 | import { resolvePage } from '~inertia' 4 | 5 | createInertiaApp({ 6 | resolve: resolvePage(() => { 7 | return import.meta.glob('./pages/**/*.vue', { eager: true }) 8 | }), 9 | setup({ el, App, props, plugin }) { 10 | createApp({ 11 | render: () => h(App, props), 12 | mounted() { 13 | document.querySelector('#app ~ a')!.style.display = 'block' 14 | }, 15 | }) 16 | .use(plugin) 17 | .mount(el) 18 | }, 19 | page: { 20 | component: 'Page1', 21 | // component: 'Page2', 22 | // component: 'my-package-1::Page3', 23 | // component: 'my-package-2::Page222', 24 | // component: 'my-package-2::Page223', 25 | // component: 'my-php-package::PhpPackagePage', 26 | props: { 27 | errors: {}, 28 | }, 29 | url: '/', 30 | version: null, 31 | scrollRegions: [], 32 | rememberedState: {}, 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /playground/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | visit /__inspect/ to inspect the intermediate state 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "cross-env DEBUG=inertia-page-loader:* vite", 5 | "build": "cross-env DEBUG=inertia-page-loader:* vue-tsc --noEmit && vite build", 6 | "serve": "cross-env DEBUG=inertia-page-loader:* vite preview" 7 | }, 8 | "dependencies": { 9 | "@inertiajs/vue3": "^1.2.0", 10 | "vue": "^3.5.0" 11 | }, 12 | "devDependencies": { 13 | "@vitejs/plugin-vue": "^5.1.0", 14 | "cross-env": "^7.0.3", 15 | "inertia-page-loader": "workspace:*", 16 | "typescript": "~5.5.0", 17 | "vite": "^5.4.0", 18 | "vite-plugin-inspect": "^0.8.7", 19 | "vue-tsc": "^2.1.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /playground/pages/Page1.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/pages/Page2.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/test_node_modules/my-plugin1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-plugin1" 3 | } 4 | -------------------------------------------------------------------------------- /playground/test_node_modules/my-plugin1/src/Pages/Page3.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/test_node_modules/my-plugin2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-plugin2", 3 | "inertia": { 4 | "my-package-2": "src/other-pages" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /playground/test_node_modules/my-plugin2/src/other-pages/Page222.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/test_node_modules/my-plugin2/src/other-pages/Page223.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /playground/test_vendor/ycs77/my-php-package/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ycs77/my-php-package", 3 | "extra": { 4 | "inertia": { 5 | "my-php-package": "resources/js/Pages" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /playground/test_vendor/ycs77/my-php-package/resources/js/Pages/PhpPackagePage.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Vue from '@vitejs/plugin-vue' 3 | import Inspect from 'vite-plugin-inspect' 4 | import InertiaPageLoader from 'inertia-page-loader/vite' 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | Inspect(), 9 | Vue(), 10 | InertiaPageLoader({ 11 | namespaces: ({ npm, composer }) => [ 12 | { 'my-package-1': 'test_node_modules/my-plugin1/src/Pages' }, 13 | npm('my-plugin2', 'test_node_modules'), 14 | composer('ycs77/my-php-package', 'test_vendor'), 15 | ], 16 | }), 17 | ], 18 | }) 19 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | -------------------------------------------------------------------------------- /scripts/postbuild.ts: -------------------------------------------------------------------------------- 1 | import { basename, dirname, resolve } from 'node:path' 2 | import { promises as fs } from 'node:fs' 3 | import { fileURLToPath } from 'node:url' 4 | import fg from 'fast-glob' 5 | import chalk from 'chalk' 6 | 7 | async function run(): Promise { 8 | const files = await fg('*.cjs', { 9 | ignore: ['chunk-*'], 10 | absolute: true, 11 | cwd: resolve(dirname(fileURLToPath(import.meta.url)), '../dist'), 12 | }) 13 | for (const file of files) { 14 | console.log(chalk.cyan.inverse(' POST '), `Fix ${basename(file)}`) 15 | 16 | // fix cjs exports 17 | let code = await fs.readFile(file, 'utf8') 18 | if (code.includes('exports.default = ') && !code.includes('module.exports = exports.default;')) { 19 | code += 'module.exports = exports.default;\n' 20 | await fs.writeFile(file, code) 21 | } 22 | } 23 | } 24 | 25 | run() 26 | -------------------------------------------------------------------------------- /src/farm.ts: -------------------------------------------------------------------------------- 1 | import { createFarmPlugin } from 'unplugin' 2 | import { unpluginFactory } from '.' 3 | 4 | export default createFarmPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createUnplugin } from 'unplugin' 2 | import type { UnpluginContextMeta, UnpluginFactory } from 'unplugin' 3 | import { pageLoader } from './page' 4 | import { isViteLike, isWebpackLike } from './utils' 5 | import type { Options, ResolvedOptions } from './types' 6 | 7 | const ids = [ 8 | '/~inertia', 9 | '~inertia', 10 | 'virtual:inertia', 11 | 'virtual/inertia', 12 | ] 13 | 14 | function resolveOptions(options: Options, meta: UnpluginContextMeta) { 15 | let extensions = options.extensions 16 | if (isViteLike(meta.framework) && !extensions) { 17 | extensions = 'vue' 18 | } else if (isWebpackLike(meta.framework) && Array.isArray(extensions)) { 19 | extensions = extensions[0] 20 | } 21 | extensions = (Array.isArray(extensions) ? extensions : [extensions ?? '']) as string[] 22 | extensions = extensions.map(ext => ext.replace(/^\./, '')) 23 | 24 | return Object.assign({ 25 | cwd: process.cwd(), 26 | namespaces: [], 27 | separator: '::', 28 | extensions: [''], 29 | import: false, 30 | ssr: false, 31 | }, options, { 32 | extensions, 33 | }) as ResolvedOptions 34 | } 35 | 36 | export const unpluginFactory: UnpluginFactory = (userOptions, meta) => { 37 | const options = resolveOptions(userOptions || {}, meta) 38 | 39 | return { 40 | name: 'inertia-page-loader', 41 | enforce: 'pre', 42 | resolveId(id) { 43 | if (ids.includes(id)) { 44 | return ids[1] 45 | } 46 | }, 47 | load(id: string) { 48 | if (ids.includes(id)) { 49 | const code = pageLoader(options, meta) 50 | return { 51 | code, 52 | map: { version: 3, mappings: '', sources: [] } as any, 53 | } 54 | } 55 | }, 56 | } 57 | } 58 | 59 | export const unplugin = /* #__PURE__ */ createUnplugin(unpluginFactory) 60 | 61 | export default unplugin 62 | -------------------------------------------------------------------------------- /src/module/files.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path' 2 | import fg from 'fast-glob' 3 | import Debug from 'debug' 4 | 5 | const debug = Debug('inertia-page-loader:module:files') 6 | 7 | export interface GetPageFilesOptions { 8 | /** 9 | * The current working directory in which to search. 10 | */ 11 | cwd?: string 12 | 13 | /** 14 | * Valid file extensions for page components. 15 | */ 16 | extensions?: string[] 17 | 18 | /** 19 | * List of path globs to exclude when resolving pages. 20 | */ 21 | exclude?: string[] 22 | } 23 | 24 | function getIgnore(exclude: string[]) { 25 | return ['.git', '**/__*__/**', ...exclude] 26 | } 27 | 28 | function extsToGlob(extensions: string[]) { 29 | return extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0] || '' 30 | } 31 | 32 | export function getPageFiles(path: string, options: GetPageFilesOptions = {}): string[] { 33 | const { 34 | extensions = [''], 35 | exclude = [], 36 | } = options 37 | 38 | const ext = extsToGlob(extensions) 39 | 40 | const files = fg.sync(join(path, `**/*.${ext}`).replaceAll('\\', '/'), { 41 | cwd: options.cwd, 42 | ignore: getIgnore(exclude), 43 | onlyFiles: true, 44 | }) 45 | 46 | debug(files) 47 | 48 | return files 49 | } 50 | -------------------------------------------------------------------------------- /src/module/import.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import { type GetPageFilesOptions, getPageFiles } from './files' 3 | 4 | const debug = { 5 | pages: Debug('inertia-page-loader:module:import:pages'), 6 | imports: Debug('inertia-page-loader:module:import:imports'), 7 | } 8 | 9 | export interface GenerateImportGlobCodeOptions extends GetPageFilesOptions { 10 | /** 11 | * The imported page number of start. 12 | */ 13 | start?: number 14 | 15 | /** 16 | * Whether the module is eagerly loaded. 17 | */ 18 | eager?: boolean 19 | } 20 | 21 | export function generateImportGlobCode(pattern: string, options: GenerateImportGlobCodeOptions = {}) { 22 | const { eager = false } = options 23 | let start = options.start ?? 0 24 | 25 | const files = getPageFiles(pattern, options).map(file => { 26 | // remove '/' and './' 27 | file = file.replace(/^(\/|\.\/)/, '') 28 | if (!file.startsWith('.')) { 29 | file = `/${file}` 30 | } 31 | return file 32 | }) 33 | 34 | const imporetedModules: Record = {} 35 | 36 | let pages = files.map(file => { 37 | if (eager) { 38 | imporetedModules[file] = `__import_page_${start}__` 39 | start++ 40 | return ` "${file}": () => import(${imporetedModules[file]}),\n` 41 | } 42 | return ` "${file}": () => import("${file}"),\n` 43 | }).join('') 44 | pages = `{\n${pages} }` 45 | 46 | debug.pages(pages) 47 | 48 | let imports = '' 49 | if (eager) { 50 | imports = files.map(file => `import ${imporetedModules[file]} from '${file}'`).join('\n') 51 | } 52 | 53 | debug.imports(imports) 54 | 55 | return { imports, pages, start } 56 | } 57 | -------------------------------------------------------------------------------- /src/page/generate-namespaces.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import Debug from 'debug' 3 | import { generateImportGlobCode } from '../module/import' 4 | import { isViteLike, isWebpackLike } from '../utils' 5 | import type { GenerateNamespacesCodeContextMeta, ResolvedOptions } from '../types' 6 | import { resolveNamespaces } from './namespace-option' 7 | 8 | const debug = Debug('inertia-page-loader:page:generate-namespaces') 9 | 10 | export function generateNamespacesCode(options: ResolvedOptions, meta: GenerateNamespacesCodeContextMeta) { 11 | const cwd = options.cwd 12 | const namespaces = resolveNamespaces(cwd, options.namespaces) 13 | 14 | let importsCode = '' 15 | let importStartNum = 0 16 | 17 | let namespacesCode = Object.keys(namespaces).map(namespace => { 18 | const modules = namespaces[namespace] 19 | 20 | let code = ` '${namespace}': [\n` 21 | if (isViteLike(meta.framework)) { 22 | modules.forEach(moduleDir => { 23 | const { imports, pages, start } = generateImportGlobCode(moduleDir, { 24 | start: importStartNum, 25 | eager: options.ssr, 26 | cwd, 27 | extensions: options.extensions, 28 | }) 29 | importStartNum = start 30 | importsCode += `${importsCode ? '\n' : ''}${imports}` 31 | code += ` name => resolveVitePage(name, ${pages}, false),\n` 32 | }) 33 | } else if (isWebpackLike(meta.framework)) { 34 | modules.forEach(moduleDir => { 35 | const moduleImporter = options.ssr || (!options.ssr && !options.import) ? 'require' : 'import' 36 | const extension = options.extensions[0] ? `.${options.extensions[0]}` : '' 37 | 38 | let modulePath = path.relative(cwd, path.resolve(cwd, moduleDir, `\${name}${extension}`)).replace(/\\/g, '/') 39 | if (!modulePath.startsWith('.')) 40 | modulePath = `./${modulePath}` 41 | 42 | code += ` name => ${moduleImporter}(\`${modulePath}\`),\n` 43 | }) 44 | } 45 | code += ' ],\n' 46 | 47 | return code 48 | }).join('') 49 | 50 | namespacesCode = `{\n${namespacesCode} }` 51 | 52 | debug(namespacesCode) 53 | debug(importsCode) 54 | 55 | return { namespacesCode, importsCode } 56 | } 57 | -------------------------------------------------------------------------------- /src/page/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable style/indent */ 2 | import type { UnpluginContextMeta } from 'unplugin' 3 | import type { ResolvedOptions } from '../types' 4 | import { isViteLike } from '../utils' 5 | import { generateNamespacesCode } from './generate-namespaces' 6 | 7 | export function pageLoader(options: ResolvedOptions, meta: UnpluginContextMeta) { 8 | const { namespacesCode, importsCode } = generateNamespacesCode(options, meta) 9 | 10 | return ` 11 | ${importsCode} 12 | 13 | export function resolvePage(resolver, transformPage) { 14 | return async name => { 15 | let page = await resolvePluginPage(name) 16 | if (!page) { 17 | page = ${ 18 | isViteLike(meta.framework) 19 | ? 'await resolveVitePage(name, await resolver(name.replace(\'.\', \'/\')))' 20 | : 'await resolver(name.replace(\'.\', \'/\'))' 21 | } 22 | } 23 | page = page.default || page 24 | if (transformPage) { 25 | page = transformPage(page, name) 26 | } 27 | return page 28 | } 29 | } 30 | 31 | export async function resolvePluginPage(name) { 32 | if (name.includes('${options.separator}')) { 33 | const [namespace, page] = name.split('${options.separator}') 34 | const meta = { framework: '${meta.framework}' } 35 | 36 | if (namespace && page) { 37 | const namespaces = ${namespacesCode} 38 | 39 | /* Load namespaces on runtime from window global variable. */ 40 | if (window.InertiaPages) { 41 | for (const namespaceGroup of window.InertiaPages.namespaces) { 42 | for (const namespace in namespaceGroup) { 43 | namespaces[namespace] = (namespaces[namespace] || []).concat(namespaceGroup[namespace]) 44 | } 45 | } 46 | } 47 | 48 | if (!namespaces[namespace]) { 49 | throw new Error(\`[inertia-page-loader]: Namespace "\${namespace}" not found\`) 50 | } 51 | 52 | for (const importedNamespace of namespaces[namespace]) { 53 | if (importedNamespace && typeof importedNamespace === 'function') { 54 | return await importedNamespace(page, meta) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | export async function resolveVitePage(name, pages, throwNotFoundError = true) { 62 | for (const path in pages) { 63 | if (${JSON.stringify(options.extensions)}.some(ext => path.endsWith(\`\${name.replaceAll('.', '/')}.\${ext}\`))) { 64 | const module = typeof pages[path] === 'function' 65 | ? pages[path]() 66 | : pages[path] 67 | 68 | return await Promise.resolve(module).then(module => module.default || module) 69 | } 70 | } 71 | 72 | if (throwNotFoundError) { 73 | throw new Error(\`[inertia-page-loader]: Page "\${name}" not found\`) 74 | } 75 | }` 76 | } 77 | -------------------------------------------------------------------------------- /src/page/namespace-option.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import type { Namespace, Namespaces, ResolvedNamespace } from '../types' 4 | 5 | export interface PackageNamespaceExtractorOptions { 6 | name: string 7 | filename: string 8 | dir: string 9 | cwd: string 10 | parse: (content: string) => Namespace 11 | } 12 | 13 | export function createPackageNamespaceExtractor(options: PackageNamespaceExtractorOptions) { 14 | return (pkg: string, dir: string = options.dir) => { 15 | const fullpath = path.resolve(options.cwd, dir, pkg, options.filename) 16 | if (!fs.existsSync(fullpath)) { 17 | throw new Error(`[inertia-page-loader]: The ${options.name} "${pkg}" does not exist`) 18 | } 19 | 20 | const namespaces = options.parse(fs.readFileSync(fullpath, { encoding: 'utf-8' })) 21 | if (!namespaces) { 22 | throw new Error(`[inertia-page-loader]: The ${options.filename} parse error of "${pkg}"`) 23 | } 24 | 25 | for (const namespace in namespaces) { 26 | const mod = namespaces[namespace] 27 | if (Array.isArray(mod)) { 28 | namespaces[namespace] = mod.map(mod => path.join(dir, pkg, mod).replaceAll('\\', '/')) 29 | } else { 30 | namespaces[namespace] = path.join(dir, pkg, mod).replaceAll('\\', '/') 31 | } 32 | } 33 | return namespaces 34 | } 35 | } 36 | 37 | export function createNpm(cwd: string = process.cwd()) { 38 | return createPackageNamespaceExtractor({ 39 | name: 'NPM package', 40 | filename: 'package.json', 41 | cwd, 42 | dir: 'node_modules', 43 | parse(content) { 44 | return JSON.parse(content).inertia 45 | }, 46 | }) 47 | } 48 | 49 | export function createComposer(cwd: string = process.cwd()) { 50 | return createPackageNamespaceExtractor({ 51 | name: 'Composer package', 52 | filename: 'composer.json', 53 | cwd, 54 | dir: 'vendor', 55 | parse(content) { 56 | return JSON.parse(content).extra.inertia 57 | }, 58 | }) 59 | } 60 | 61 | export function resolveNamespaces(cwd: string, namespaces: Namespaces): ResolvedNamespace { 62 | const resolvedNamespaces = typeof namespaces === 'function' 63 | ? namespaces({ 64 | npm: createNpm(cwd), 65 | composer: createComposer(cwd), 66 | }) 67 | : namespaces 68 | 69 | const output = {} as ResolvedNamespace 70 | 71 | for (const obj of resolvedNamespaces) { 72 | for (const key of Object.keys(obj)) { 73 | output[key] = Array.isArray(obj[key]) 74 | ? (obj[key] as string[]) 75 | : ([obj[key]] as string[]) 76 | } 77 | } 78 | 79 | return output 80 | } 81 | -------------------------------------------------------------------------------- /src/rollup.ts: -------------------------------------------------------------------------------- 1 | import unplugin from '.' 2 | 3 | export default unplugin.rollup 4 | -------------------------------------------------------------------------------- /src/rspack.ts: -------------------------------------------------------------------------------- 1 | import { createRspackPlugin } from 'unplugin' 2 | import { unpluginFactory } from '.' 3 | 4 | export default createRspackPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | import InertiaPages from './plugin' 2 | 3 | if (!window.InertiaPages) { 4 | // @ts-ignore 5 | window.InertiaPages = new InertiaPages() 6 | } 7 | 8 | export { InertiaPages } 9 | -------------------------------------------------------------------------------- /src/runtime/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { PageResolver } from '../types' 2 | 3 | export default class InertiaPages { 4 | private _namespaces: Record[] = [] 5 | 6 | addNamespace(namespace: string, resolver: PageResolver | PageResolver[]) { 7 | this._namespaces.push({ [namespace]: resolver }) 8 | return this 9 | } 10 | 11 | get namespaces() { 12 | return this._namespaces 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | // @ts-ignore 3 | // eslint-disable-next-line vars-on-top, no-var 4 | var InertiaPages: InstanceType 5 | } 6 | 7 | export interface Options { 8 | /** 9 | * Current work directory. 10 | * 11 | * @default process.cwd() 12 | */ 13 | cwd?: string 14 | 15 | /** 16 | * Define namespace mapping. 17 | * 18 | * @default [] 19 | */ 20 | namespaces?: Namespaces 21 | 22 | /** 23 | * Namespace separator. 24 | * 25 | * @default '::' 26 | */ 27 | separator?: string 28 | 29 | /** 30 | * Module extensions. 31 | * 32 | * vite: 33 | * @type {string|string[]} 34 | * @default 'vue' 35 | * 36 | * webpack: 37 | * @type {string} 38 | * @default '' 39 | */ 40 | extensions?: string | string[] 41 | 42 | /** 43 | * Use `import()` to load pages for webpack, default is using `require()`. 44 | * Only for webpack. 45 | * 46 | * @default false 47 | */ 48 | import?: boolean 49 | 50 | /** 51 | * Enable SSR mode. 52 | * 53 | * @default false 54 | */ 55 | ssr?: boolean 56 | } 57 | 58 | export type ResolvedOptions = Required> & { 59 | /** 60 | * Module extensions. 61 | */ 62 | extensions: string[] 63 | } 64 | 65 | export type PageResolver = (name: string) => T | Promise 66 | 67 | export type Namespace = Record 68 | export type ResolvedNamespace = Record 69 | export interface NamespacesArgs { 70 | npm: (pkg: string, dir: string) => Namespace 71 | composer: (pkg: string, dir: string) => Namespace 72 | } 73 | export type Namespaces = Namespace[] | ((args: NamespacesArgs) => Namespace[]) 74 | 75 | export interface GenerateNamespacesCodeContextMeta { 76 | framework: 'rollup' | 'vite' | 'rolldown' | 'webpack' | 'esbuild' | 'rspack' | 'farm' 77 | } 78 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateNamespacesCodeContextMeta } from './types' 2 | 3 | export function isViteLike(framework: GenerateNamespacesCodeContextMeta['framework']) { 4 | return ['vite', 'rolldown', 'farm'].includes(framework) 5 | } 6 | 7 | export function isRollupLike(framework: GenerateNamespacesCodeContextMeta['framework']) { 8 | return ['rollup', 'vite', 'rolldown', 'farm'].includes(framework) 9 | } 10 | 11 | export function isWebpackLike(framework: GenerateNamespacesCodeContextMeta['framework']) { 12 | return ['webpack', 'rspack'].includes(framework) 13 | } 14 | -------------------------------------------------------------------------------- /src/vite.ts: -------------------------------------------------------------------------------- 1 | import { createVitePlugin } from 'unplugin' 2 | import { unpluginFactory } from '.' 3 | 4 | export default createVitePlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /src/webpack.ts: -------------------------------------------------------------------------------- 1 | import { createWebpackPlugin } from 'unplugin' 2 | import { unpluginFactory } from '.' 3 | 4 | export default createWebpackPlugin(unpluginFactory) 5 | -------------------------------------------------------------------------------- /tests/generate-namespaces.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { describe, expect, it } from 'vitest' 3 | import { generateNamespacesCode } from '../src/page/generate-namespaces' 4 | import type { Namespaces, ResolvedOptions } from '../src/types' 5 | 6 | describe('generate namespaces', () => { 7 | const baseOptions = { 8 | cwd: path.join(process.cwd(), 'tests'), 9 | namespaces: [], 10 | separator: '::', 11 | extensions: ['vue'], 12 | import: false, 13 | ssr: false, 14 | } 15 | 16 | const namespaces = [ 17 | { 'my-package-1': 'test_node_modules/my-plugin1/src/Pages' }, 18 | { 'my-package-2': 'test_node_modules/my-plugin2/src/other-pages' }, 19 | { 'my-php-package': 'test_vendor/ycs77/my-php-package/resources/js/Pages' }, 20 | ] 21 | 22 | it('simple options', () => { 23 | const code = generateNamespacesCode(baseOptions, { framework: 'vite' }) 24 | 25 | expect(code.importsCode).toMatchInlineSnapshot('""') 26 | expect(code.namespacesCode).toMatchInlineSnapshot(` 27 | "{ 28 | }" 29 | `) 30 | }) 31 | 32 | it('namespaces with vite', () => { 33 | const options = { 34 | ...baseOptions, 35 | namespaces, 36 | } 37 | 38 | const code = generateNamespacesCode(options, { framework: 'vite' }) 39 | 40 | expect(code.importsCode).toMatchInlineSnapshot('""') 41 | expect(code.namespacesCode).toMatchInlineSnapshot(` 42 | "{ 43 | 'my-package-1': [ 44 | name => resolveVitePage(name, { 45 | "/test_node_modules/my-plugin1/src/Pages/Page3.vue": () => import("/test_node_modules/my-plugin1/src/Pages/Page3.vue"), 46 | }, false), 47 | ], 48 | 'my-package-2': [ 49 | name => resolveVitePage(name, { 50 | "/test_node_modules/my-plugin2/src/other-pages/Page222.vue": () => import("/test_node_modules/my-plugin2/src/other-pages/Page222.vue"), 51 | "/test_node_modules/my-plugin2/src/other-pages/Page223.vue": () => import("/test_node_modules/my-plugin2/src/other-pages/Page223.vue"), 52 | }, false), 53 | ], 54 | 'my-php-package': [ 55 | name => resolveVitePage(name, { 56 | "/test_vendor/ycs77/my-php-package/resources/js/Pages/PhpPackagePage.vue": () => import("/test_vendor/ycs77/my-php-package/resources/js/Pages/PhpPackagePage.vue"), 57 | }, false), 58 | ], 59 | }" 60 | `) 61 | }) 62 | 63 | it('vite with ssr', () => { 64 | const options = { 65 | ...baseOptions, 66 | namespaces, 67 | ssr: true, 68 | } 69 | 70 | const code = generateNamespacesCode(options, { framework: 'vite' }) 71 | 72 | expect(code.importsCode).toMatchInlineSnapshot(` 73 | "import __import_page_0__ from '/test_node_modules/my-plugin1/src/Pages/Page3.vue' 74 | import __import_page_1__ from '/test_node_modules/my-plugin2/src/other-pages/Page222.vue' 75 | import __import_page_2__ from '/test_node_modules/my-plugin2/src/other-pages/Page223.vue' 76 | import __import_page_3__ from '/test_vendor/ycs77/my-php-package/resources/js/Pages/PhpPackagePage.vue'" 77 | `) 78 | expect(code.namespacesCode).toMatchInlineSnapshot(` 79 | "{ 80 | 'my-package-1': [ 81 | name => resolveVitePage(name, { 82 | "/test_node_modules/my-plugin1/src/Pages/Page3.vue": () => import(__import_page_0__), 83 | }, false), 84 | ], 85 | 'my-package-2': [ 86 | name => resolveVitePage(name, { 87 | "/test_node_modules/my-plugin2/src/other-pages/Page222.vue": () => import(__import_page_1__), 88 | "/test_node_modules/my-plugin2/src/other-pages/Page223.vue": () => import(__import_page_2__), 89 | }, false), 90 | ], 91 | 'my-php-package': [ 92 | name => resolveVitePage(name, { 93 | "/test_vendor/ycs77/my-php-package/resources/js/Pages/PhpPackagePage.vue": () => import(__import_page_3__), 94 | }, false), 95 | ], 96 | }" 97 | `) 98 | }) 99 | 100 | it('namespaces with webpack', () => { 101 | const options = { 102 | ...baseOptions, 103 | namespaces, 104 | extensions: [''], 105 | } 106 | 107 | const code = generateNamespacesCode(options, { framework: 'webpack' }) 108 | 109 | expect(code.importsCode).toMatchInlineSnapshot('""') 110 | expect(code.namespacesCode).toMatchInlineSnapshot(` 111 | "{ 112 | 'my-package-1': [ 113 | name => require(\`./test_node_modules/my-plugin1/src/Pages/\${name}\`), 114 | ], 115 | 'my-package-2': [ 116 | name => require(\`./test_node_modules/my-plugin2/src/other-pages/\${name}\`), 117 | ], 118 | 'my-php-package': [ 119 | name => require(\`./test_vendor/ycs77/my-php-package/resources/js/Pages/\${name}\`), 120 | ], 121 | }" 122 | `) 123 | }) 124 | 125 | it('webpack with import', () => { 126 | const options = { 127 | ...baseOptions, 128 | namespaces, 129 | extensions: [''], 130 | import: true, 131 | } 132 | 133 | const code = generateNamespacesCode(options, { framework: 'webpack' }) 134 | 135 | expect(code.importsCode).toMatchInlineSnapshot('""') 136 | expect(code.namespacesCode).toMatchInlineSnapshot(` 137 | "{ 138 | 'my-package-1': [ 139 | name => import(\`./test_node_modules/my-plugin1/src/Pages/\${name}\`), 140 | ], 141 | 'my-package-2': [ 142 | name => import(\`./test_node_modules/my-plugin2/src/other-pages/\${name}\`), 143 | ], 144 | 'my-php-package': [ 145 | name => import(\`./test_vendor/ycs77/my-php-package/resources/js/Pages/\${name}\`), 146 | ], 147 | }" 148 | `) 149 | }) 150 | }) 151 | -------------------------------------------------------------------------------- /tests/namespace-option.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { describe, expect, it } from 'vitest' 3 | import { createComposer, createNpm } from '../src/page/namespace-option' 4 | 5 | describe('namespace option', () => { 6 | const cwd = path.resolve(process.cwd(), 'tests').replaceAll('\\', '/') 7 | 8 | it('resolve NPM namespace', () => { 9 | const npm = createNpm(cwd) 10 | const namespace = npm('my-plugin2', 'test_node_modules') 11 | 12 | expect(namespace).toMatchInlineSnapshot(` 13 | { 14 | "my-package-2": "test_node_modules/my-plugin2/src/other-pages", 15 | } 16 | `) 17 | }) 18 | 19 | it('resolve Composer namespace', () => { 20 | const composer = createComposer(cwd) 21 | const namespace = composer('ycs77/my-php-package', 'test_vendor') 22 | 23 | expect(namespace).toMatchInlineSnapshot(` 24 | { 25 | "my-php-package": "test_vendor/ycs77/my-php-package/resources/js/Pages", 26 | } 27 | `) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/test_node_modules/my-plugin1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-plugin1" 3 | } 4 | -------------------------------------------------------------------------------- /tests/test_node_modules/my-plugin1/src/Pages/Page3.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/test_node_modules/my-plugin2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-plugin2", 3 | "inertia": { 4 | "my-package-2": "src/other-pages" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/test_node_modules/my-plugin2/src/other-pages/Page222.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/test_node_modules/my-plugin2/src/other-pages/Page223.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/test_vendor/ycs77/my-php-package/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ycs77/my-php-package", 3 | "extra": { 4 | "inertia": { 5 | "my-php-package": "resources/js/Pages" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_vendor/ycs77/my-php-package/resources/js/Pages/PhpPackagePage.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["ESNext", "DOM"], 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["dist", "eslint.config.js"] 13 | } 14 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup' 2 | 3 | export default { 4 | entry: ['src/*.ts'], 5 | clean: true, 6 | format: ['cjs', 'esm'], 7 | outExtension({ format }) { 8 | if (format === 'cjs') { 9 | return { js: '.cjs' } 10 | } else if (format === 'esm') { 11 | return { js: '.mjs' } 12 | } 13 | return {} 14 | }, 15 | dts: true, 16 | cjsInterop: true, 17 | splitting: true, 18 | onSuccess: 'npm run build:fix', 19 | } satisfies Options 20 | -------------------------------------------------------------------------------- /tsup.runtime-config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup' 2 | 3 | export default { 4 | entry: { 5 | runtime: 'src/runtime/index.ts', 6 | }, 7 | target: 'es5', 8 | format: ['cjs', 'esm', 'iife'], 9 | minify: true, 10 | outExtension({ format }) { 11 | if (format === 'cjs') { 12 | return { js: '.cjs' } 13 | } else if (format === 'esm') { 14 | return { js: '.mjs' } 15 | } else if (format === 'iife') { 16 | return { js: '.iife.js' } 17 | } 18 | return {} 19 | }, 20 | } satisfies Options 21 | --------------------------------------------------------------------------------