├── .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 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/example/packages/base/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
4 | Welcome to desktop version!
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/packages/mobile/components/AppLanding.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Welcome to mobile version!
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/test/fixture/themes/base/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello world from base!
4 |
5 |
Color: {{ $config.color }}
6 |
7 |
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 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/packages/mobile/components/AppLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
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 |
--------------------------------------------------------------------------------