├── .eslintignore ├── test ├── fixture │ ├── app │ │ ├── components │ │ │ └── .gitkeep │ │ ├── logconfig.js │ │ ├── nuxt.config.cjs.js │ │ └── nuxt.config.ts │ └── themes │ │ ├── red │ │ ├── components │ │ │ └── .gitkeep │ │ └── nuxt.config.ts │ │ └── base │ │ ├── components │ │ └── .gitkeep │ │ ├── pages │ │ └── index.vue │ │ └── nuxt.config.ts ├── index.test.ts └── __snapshots__ │ └── index.test.ts.snap ├── renovate.json ├── .eslintrc ├── example ├── packages │ ├── base │ │ ├── layouts │ │ │ └── default.vue │ │ ├── pages │ │ │ └── index.vue │ │ ├── package.json │ │ └── nuxt.config.js │ ├── desktop │ │ ├── components │ │ │ ├── AppLanding.vue │ │ │ └── AppLayout.vue │ │ ├── nuxt.config.js │ │ └── package.json │ └── mobile │ │ ├── components │ │ ├── AppLanding.vue │ │ └── AppLayout.vue │ │ ├── nuxt.config.js │ │ └── package.json ├── package.json └── README.md ├── .gitignore ├── jest.config.js ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md ├── package.json ├── README.md └── src └── index.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /test/fixture/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixture/themes/red/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixture/themes/base/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs/eslint-config-typescript" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example/packages/base/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /example/packages/base/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixture/app/logconfig.js: -------------------------------------------------------------------------------- 1 | console.log(require('jiti')(__dirname)('./nuxt.config.ts')) 2 | -------------------------------------------------------------------------------- /test/fixture/app/nuxt.config.cjs.js: -------------------------------------------------------------------------------- 1 | export default require('jiti')(__dirname)('./nuxt.config.ts') 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | *.log 4 | .DS_Store 5 | coverage 6 | dist 7 | types 8 | .nuxt 9 | -------------------------------------------------------------------------------- /example/packages/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@app/base", 3 | "version": "0.0.0", 4 | "private": true 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | collectCoverage: true, 4 | testEnvironment: 'node' 5 | } 6 | -------------------------------------------------------------------------------- /example/packages/desktop/components/AppLanding.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/packages/mobile/components/AppLanding.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /test/fixture/themes/base/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /example/packages/base/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { nuxtConfig } from 'nuxt-extend' 2 | 3 | export default nuxtConfig({ 4 | name: '@app/base', 5 | srcDir: __dirname 6 | }) 7 | -------------------------------------------------------------------------------- /example/packages/mobile/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { nuxtConfig } from 'nuxt-extend' 2 | 3 | export default nuxtConfig({ 4 | name: '@app/mobile', 5 | extends: '@app/base/nuxt.config' 6 | }) 7 | -------------------------------------------------------------------------------- /example/packages/desktop/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { nuxtConfig } from 'nuxt-extend' 2 | 3 | export default nuxtConfig({ 4 | name: '@app/desktop', 5 | extends: '@app/base/nuxt.config' 6 | }) 7 | -------------------------------------------------------------------------------- /example/packages/desktop/components/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /example/packages/mobile/components/AppLayout.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /test/fixture/app/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { nuxtConfig } from '../../../src/index' 2 | 3 | export default nuxtConfig({ 4 | name: 'MyApp', 5 | extends: '../themes/red/nuxt.config', 6 | rootDir: __dirname 7 | }) 8 | -------------------------------------------------------------------------------- /test/fixture/themes/base/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { nuxtConfig } from '../../../../src/index' 2 | 3 | export default nuxtConfig({ 4 | name: 'BaseTheme', 5 | rootDir: __dirname, 6 | srcDir: __dirname, 7 | 8 | publicRuntimeConfig: { 9 | color: 'blue' 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /example/packages/mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@app/mobile", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "analyze": "nuxt build --analyze", 7 | "build": "nuxt build", 8 | "dev": "nuxt dev", 9 | "generate": "nuxt generate" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixture/themes/red/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { nuxtConfig } from '../../../../src/index' 2 | 3 | export default nuxtConfig({ 4 | name: 'RedTheme', 5 | extends: '../base/nuxt.config', 6 | rootDir: __dirname, 7 | 8 | publicRuntimeConfig: { 9 | color: 'red' 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /example/packages/desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@app/desktop", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "analyze": "nuxt build --analyze", 7 | "build": "nuxt build", 8 | "dev": "nuxt dev", 9 | "generate": "nuxt generate" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "types": [ 9 | "@nuxt/types", 10 | "@nuxt/components", 11 | "jest" 12 | ] 13 | }, 14 | "include": [ 15 | "src" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "scripts": { 7 | "dev:desktop": "yarn workspace @app/desktop dev", 8 | "dev:mobile": "yarn workspace @app/mobile dev", 9 | "build:desktop": "yarn workspace @app/desktop dev", 10 | "build:mobile": "yarn workspace @app/mobile dev" 11 | }, 12 | "devDependencies": { 13 | "nuxt": "^2.14.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { nuxtConfig } from '../src' 2 | import config from './fixture/app/nuxt.config' 3 | 4 | const scrub = (input) => { 5 | if (typeof input === 'string') { 6 | return input.replace(__dirname, '{test}') 7 | } 8 | 9 | if (Array.isArray(input)) { 10 | return input.map(i => scrub(i)) 11 | } 12 | 13 | if (typeof input === 'object') { 14 | const res = {} 15 | for (const key in input) { 16 | res[key] = scrub(input[key]) 17 | } 18 | return res 19 | } 20 | 21 | return input 22 | } 23 | 24 | it('fails on config being a function', () => { 25 | const config = () => ({}) 26 | expect(() => nuxtConfig(config)) 27 | .toThrow('extending is not possible with nuxt config as a function') 28 | }) 29 | 30 | it('matches snapshot', () => { 31 | expect(scrub(config)).toMatchSnapshot() 32 | }) 33 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`matches snapshot 1`] = ` 4 | Object { 5 | "alias": Object { 6 | "BaseTheme": "{test}/fixture/themes/base", 7 | "MyApp": "{test}/fixture/app", 8 | "RedTheme": "{test}/fixture/themes/red", 9 | }, 10 | "components": Array [ 11 | Object { 12 | "level": 0, 13 | "path": "{test}/fixture/app/components", 14 | }, 15 | Object { 16 | "level": 1, 17 | "path": "{test}/fixture/themes/red/components", 18 | }, 19 | Object { 20 | "level": 2, 21 | "path": "{test}/fixture/themes/base/components", 22 | }, 23 | ], 24 | "hooks": Object {}, 25 | "publicRuntimeConfig": Object { 26 | "color": "red", 27 | }, 28 | "rootDir": "{test}/fixture/app", 29 | "srcDir": "{test}/fixture/themes/base", 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /.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 | ci: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | node: [14] 19 | 20 | steps: 21 | - uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node }} 24 | 25 | - name: checkout 26 | uses: actions/checkout@master 27 | 28 | - name: cache node_modules 29 | uses: actions/cache@v2 30 | with: 31 | path: node_modules 32 | key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} 33 | 34 | - name: Install dependencies 35 | if: steps.cache.outputs.cache-hit != 'true' 36 | run: yarn 37 | 38 | - name: Lint 39 | run: yarn lint 40 | 41 | - name: Test 42 | run: yarn jest 43 | 44 | - name: Coverage 45 | uses: codecov/codecov-action@v1 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [0.1.0](https://github.com/nuxt-community/nuxt-extend/compare/v0.0.3...v0.1.0) (2021-04-29) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * module exports 11 | 12 | ### Features 13 | 14 | * module exports ([c770c34](https://github.com/nuxt-community/nuxt-extend/commit/c770c34b17793a61404f189043f4bd2631c08b38)) 15 | 16 | ### [0.0.3](https://github.com/nuxt-community/nuxt-extend/compare/v0.0.2...v0.0.3) (2020-11-30) 17 | 18 | 19 | ### Features 20 | 21 | * improve merge logic ([ede481b](https://github.com/nuxt-community/nuxt-extend/commit/ede481b55e2292577959307f4618cf3fb6ea635e)) 22 | 23 | ### [0.0.2](https://github.com/nuxt-community/nuxt-extend/compare/v0.0.1...v0.0.2) (2020-11-18) 24 | 25 | ### 0.0.1 (2020-11-12) 26 | 27 | 28 | ### Features 29 | 30 | * components level ([41071f1](https://github.com/nuxt-community/nuxt-extend/commit/41071f1ef8e9e627e0f40e5ddc32eecc596f691d)) 31 | * improvements ([3d13c4f](https://github.com/nuxt-community/nuxt-extend/commit/3d13c4f82ed5b92f438b82cf778706491b42e0ec)) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-extend", 3 | "version": "0.1.0", 4 | "description": "", 5 | "repository": "nuxt-community/nuxt-extend", 6 | "license": "MIT", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.mjs", 10 | "require": "./dist/index.js" 11 | } 12 | }, 13 | "main": "./dist/index.js", 14 | "module": "./dist/index.mjs", 15 | "types": "./dist/index.d.ts", 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "siroc build", 21 | "dev": "nuxt test/fixture/app -c nuxt.config.cjs.js", 22 | "lint": "eslint --ext .ts src", 23 | "prepare": "yarn link && yarn link nuxt-extend", 24 | "prepublish": "yarn build", 25 | "release": "yarn test && yarn build && standard-version && git push --follow-tags && npm publish", 26 | "test": "yarn lint && yarn jest" 27 | }, 28 | "dependencies": { 29 | "defu": "^4.0.1", 30 | "hookable": "^4.4.1", 31 | "jiti": "^1.9.1" 32 | }, 33 | "devDependencies": { 34 | "@nuxt/components": "latest", 35 | "@nuxt/types": "latest", 36 | "@nuxt/typescript-runtime": "latest", 37 | "@nuxtjs/eslint-config-typescript": "latest", 38 | "@types/jest": "latest", 39 | "@types/node": "latest", 40 | "eslint": "latest", 41 | "jest": "latest", 42 | "nuxt": "latest", 43 | "siroc": "latest", 44 | "standard-version": "latest", 45 | "ts-jest": "latest", 46 | "typescript": "latest" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Extend example 2 | 3 | This example is using `nuxt-extend` and [nuxt components](https://nuxtjs.org/docs/2.x/features/nuxt-components) 4 | to create a multi-variant mobile/desktop nuxt application. 5 | 6 | - Using [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces) for mono-repo managenment 7 | - Most of the logic is shared in base package 8 | - Using two sperate builds ensures that there are no additional dependencies leaked between mobile and desktop variants 9 | 10 | ## Development 11 | 12 | - Install dependencies with `yarn` 13 | - Use `yarn dev:desktop` and `yarn dev:mobile` 14 | 15 | ## Deployment 16 | 17 | - Build with `yarn build:desktop` and `yarn build:mobile` 18 | - Deploy each app to a subdomain 19 | 20 | ## Configuration 21 | 22 | - Add any common nuxt module and config to base ([base/nuxt.config](./packages/base/nuxt.config.js)) 23 | - Avoid adding mobile/desktop specific modules, plugins and css 24 | - Instead use [desktop/nuxt.config](./packages/desktop/nuxt.config.js) and [mobile/nuxt.config](./packages/mobile/nuxt.config.js) 25 | 26 | ## Pages / Layouts 27 | 28 | Only [base/pages](./packages/base/pages) and [base/layouts](./packages/base/layouts) directories are supported. 29 | 30 | We use named components to implement them per-variant. 31 | 32 | ## Store 33 | 34 | Only [base/store](./packages/base/store) directory is supported. 35 | 36 | It is best to write shared logic inside vuex store modules. 37 | 38 | ## Styles 39 | 40 | It is recommended to use scoped styles. But in case that need to use global styles, 41 | they can be included in layout component or `nuxt.config` of each variant. 42 | 43 | Also for component libraries, you can include their module in each variant. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `nuxt-extend` 2 | 3 | [![npm version][npm-v-src]][npm-v-href] 4 | [![npm downloads][npm-d-src]][npm-d-href] 5 | [![ci][ci-src]][ci-href] 6 | 7 | This utility allows extending a nuxt project based on another one by smartly merging `nuxt.config` files. 8 | 9 | It can be useful if: 10 | - You want to share a base config across mono-repo projects 11 | - You want to create a multi-variant (like `mobile`/`desktop`) app 12 | - You want to create a reusable nuxt theme (like one for docs) 13 | 14 | **Note:** Proper Multi-App ([rfc](https://github.com/nuxt/rfcs/issues/30)) is comming with nuxt3 which also 15 | allows extending auto scanned directories like `pages/` and `store/` and merging them. 16 | 17 | ## Mobile/Desktop Demo 18 | 19 | See [this example](./example) 20 | 21 | ## Usage 22 | 23 | Install `nuxt-extend` as a dependency: 24 | 25 | ```sh 26 | # yarn 27 | yarn add nuxt-extend 28 | 29 | # npm 30 | npm i nuxt-extend 31 | ``` 32 | 33 | Update `nuxt.config` file: 34 | 35 | ```js 36 | import { nuxtConfig } from 'nuxt-extend' 37 | 38 | export default nuxtConfig({ 39 | /* your actual nuxt configuration */ 40 | }) 41 | ``` 42 | 43 | ### Parent 44 | 45 | Use `extends` key in `nuxt.config`: 46 | 47 | ```js 48 | import { nuxtConfig } from 'nuxt-extend' 49 | 50 | export default nuxtConfig({ 51 | extends: '', 52 | }) 53 | ``` 54 | 55 | ### Base 56 | 57 | - Update `nuxt.config` and ensure required `rootDir` and `name` properties are provided 58 | 59 | ```js 60 | import { nuxtConfig } from 'nuxt-extend' 61 | 62 | export default nuxtConfig({ 63 | rootDir: __dirname, 64 | name: 'myTheme', 65 | } 66 | ``` 67 | 68 | **Note:** If you are extending recusively, `srcDir` should be ONLY provided one one level that implements actual `pages/` and `store/` (which is usually the base). 69 | 70 | - Instead of using `~/` or `@/` aliases, use `~myTheme` or `@myTheme` 71 | 72 | ## License 73 | 74 | MIT. Made with 💖 75 | 76 | 77 | [npm-v-src]: https://img.shields.io/npm/v/nuxt-extend?style=flat-square 78 | [npm-v-href]: https://npmjs.com/package/nuxt-extend 79 | 80 | [npm-d-src]: https://img.shields.io/npm/dm/nuxt-extend?style=flat-square 81 | [npm-d-href]: https://npmjs.com/package/nuxt-extend 82 | 83 | [ci-src]: https://img.shields.io/github/workflow/status/nuxt-community/nuxt-extend/ci/master?style=flat-square 84 | [ci-href]: https://github.com/nuxt-community/nuxt-extend/actions?query=workflow%3Aci 85 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve, dirname } from 'path' 2 | import { existsSync, realpathSync } from 'fs' 3 | import Hookable, { configHooksT } from 'hookable' 4 | import defu from 'defu' 5 | import jiti from 'jiti' 6 | import type { NuxtConfig } from '@nuxt/types' 7 | 8 | declare module '@nuxt/types' { 9 | interface NuxtConfig { 10 | hooks?: configHooksT 11 | name?: string 12 | extends?: string 13 | alias?: { [key: string]: string } 14 | } 15 | } 16 | 17 | export function nuxtConfig (config: NuxtConfig): NuxtConfig { 18 | config = resolveConfig(config, 0) 19 | 20 | delete config._file 21 | delete config._dir 22 | delete config.name 23 | delete config.extends 24 | 25 | return config 26 | } 27 | 28 | function resolveConfig (config: NuxtConfig, level) { 29 | if (typeof config === 'function') { 30 | throw new TypeError('extending is not possible with nuxt config as a function') 31 | } 32 | 33 | const dir = config.srcDir || config.rootDir || config._dir || process.cwd() 34 | 35 | if (dir && config.name) { 36 | config.alias = config.alias || {} 37 | config.alias[config.name] = dir 38 | } 39 | 40 | if (dir && config.components === undefined) { 41 | config.components = [] 42 | const componentsDir = resolve(dir, 'components') 43 | if (existsSync(componentsDir)) { 44 | config.components.push({ path: componentsDir }) 45 | } 46 | const globalComponentsDir = resolve(dir, 'components/global') 47 | if (existsSync(globalComponentsDir)) { 48 | config.components.push({ path: globalComponentsDir, global: true }) 49 | } 50 | } 51 | 52 | if (config.extends) { 53 | const base = loadConfig(config.extends, dir) 54 | return mergeConfig(config, resolveConfig(base, level + 1)) 55 | } 56 | 57 | return config 58 | } 59 | 60 | function loadConfig (configFile: string, from: string): NuxtConfig { 61 | const _require = jiti(from) 62 | configFile = realpathSync(_require.resolve(configFile)) 63 | 64 | let config = _require(configFile) 65 | config = (config.default || config) as NuxtConfig 66 | config._file = configFile 67 | config._dir = dirname(configFile) 68 | 69 | return config 70 | } 71 | 72 | function mergeConfig (target: NuxtConfig, base: NuxtConfig): NuxtConfig { 73 | // Custom merges 74 | const override: NuxtConfig = {} 75 | 76 | // Merge hooks 77 | override.hooks = Hookable.mergeHooks(base.hooks || {}, target.hooks || {}) 78 | 79 | // Merge components 80 | if (base.components || target.components) { 81 | override.components = [ 82 | ...normalizeComponents(target.components), 83 | ...normalizeComponents(base.components, true) 84 | ] 85 | } 86 | 87 | // Mege with defu 88 | return { ...defu.arrayFn(target, base), ...override } 89 | } 90 | 91 | function normalizeComponents (components: NuxtConfig['components'], isBase?: boolean) { 92 | if (typeof components === 'boolean' || !components) { 93 | components = [] 94 | } 95 | 96 | if (!Array.isArray(components)) { 97 | // TODO: Deprecate components: { dirs } support from @nuxt/components 98 | throw new TypeError('`components` should be an array: ' + typeof components) 99 | } 100 | 101 | const componentsArr = components.map(dir => ({ 102 | ...(typeof dir === 'string' ? { path: dir } : dir) 103 | })) 104 | 105 | for (const component of componentsArr) { 106 | component.level = (component.level || 0) + (isBase ? 1 : 0) 107 | } 108 | 109 | return componentsArr 110 | } 111 | --------------------------------------------------------------------------------