├── .babelrc ├── .changeset ├── README.md └── config.json ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .babelrc ├── README.md ├── next.config.js ├── package.json ├── pages │ ├── _app.jsx │ ├── _layout.jsx │ ├── dashboard │ │ ├── _layout.jsx │ │ └── user │ │ │ ├── [id].jsx │ │ │ ├── _layout.jsx │ │ │ └── index.jsx │ └── index.jsx └── patches │ └── babel-plugin-codegen+4.1.5.patch ├── package.json ├── src └── index.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "12.22.0" 8 | }, 9 | "bugfixes": true 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | example/yarn.lock 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,osx,linux,vim 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,osx,linux,vim 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### Node ### 21 | # Logs 22 | logs 23 | *.log 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | lerna-debug.log* 28 | .pnpm-debug.log* 29 | 30 | # Diagnostic reports (https://nodejs.org/api/report.html) 31 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 32 | 33 | # Runtime data 34 | pids 35 | *.pid 36 | *.seed 37 | *.pid.lock 38 | 39 | # Directory for instrumented libs generated by jscoverage/JSCover 40 | lib-cov 41 | 42 | # Coverage directory used by tools like istanbul 43 | coverage 44 | *.lcov 45 | 46 | # nyc test coverage 47 | .nyc_output 48 | 49 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 50 | .grunt 51 | 52 | # Bower dependency directory (https://bower.io/) 53 | bower_components 54 | 55 | # node-waf configuration 56 | .lock-wscript 57 | 58 | # Compiled binary addons (https://nodejs.org/api/addons.html) 59 | build/Release 60 | 61 | # Dependency directories 62 | node_modules/ 63 | jspm_packages/ 64 | 65 | # Snowpack dependency directory (https://snowpack.dev/) 66 | web_modules/ 67 | 68 | # TypeScript cache 69 | *.tsbuildinfo 70 | 71 | # Optional npm cache directory 72 | .npm 73 | 74 | # Optional eslint cache 75 | .eslintcache 76 | 77 | # Microbundle cache 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | .node_repl_history 85 | 86 | # Output of 'npm pack' 87 | *.tgz 88 | 89 | # Yarn Integrity file 90 | .yarn-integrity 91 | 92 | # dotenv environment variables file 93 | .env 94 | .env.test 95 | .env.production 96 | 97 | # parcel-bundler cache (https://parceljs.org/) 98 | .cache 99 | .parcel-cache 100 | 101 | # Next.js build output 102 | .next 103 | out 104 | 105 | # Nuxt.js build / generate output 106 | .nuxt 107 | dist 108 | 109 | # Gatsby files 110 | .cache/ 111 | # Comment in the public line in if your project uses Gatsby and not Next.js 112 | # https://nextjs.org/blog/next-9-1#public-directory-support 113 | # public 114 | 115 | # vuepress build output 116 | .vuepress/dist 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # Stores VSCode versions used for testing VSCode extensions 131 | .vscode-test 132 | 133 | # yarn v2 134 | .yarn/cache 135 | .yarn/unplugged 136 | .yarn/build-state.yml 137 | .yarn/install-state.gz 138 | .pnp.* 139 | 140 | ### Node Patch ### 141 | # Serverless Webpack directories 142 | .webpack/ 143 | 144 | # Optional stylelint cache 145 | .stylelintcache 146 | 147 | # SvelteKit build / generate output 148 | .svelte-kit 149 | 150 | ### OSX ### 151 | # General 152 | .DS_Store 153 | .AppleDouble 154 | .LSOverride 155 | 156 | # Icon must end with two \r 157 | Icon 158 | 159 | 160 | # Thumbnails 161 | ._* 162 | 163 | # Files that might appear in the root of a volume 164 | .DocumentRevisions-V100 165 | .fseventsd 166 | .Spotlight-V100 167 | .TemporaryItems 168 | .Trashes 169 | .VolumeIcon.icns 170 | .com.apple.timemachine.donotpresent 171 | 172 | # Directories potentially created on remote AFP share 173 | .AppleDB 174 | .AppleDesktop 175 | Network Trash Folder 176 | Temporary Items 177 | .apdisk 178 | 179 | ### Vim ### 180 | # Swap 181 | [._]*.s[a-v][a-z] 182 | !*.svg # comment out if you don't need vector files 183 | [._]*.sw[a-p] 184 | [._]s[a-rt-v][a-z] 185 | [._]ss[a-gi-z] 186 | [._]sw[a-p] 187 | 188 | # Session 189 | Session.vim 190 | Sessionx.vim 191 | 192 | # Temporary 193 | .netrwhist 194 | # Auto-generated tag files 195 | tags 196 | # Persistent undo 197 | [._]*.un~ 198 | 199 | # End of https://www.toptal.com/developers/gitignore/api/node,osx,linux,vim 200 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @ceteio/next-layout-loader 2 | 3 | ## 2.0.1 4 | 5 | ### Patch Changes 6 | 7 | - 506f933: Optimise rendering by correctly limiting the maximum number of layout files to those that are actually present in the pages/ directory. 8 | 9 | ## 2.0.0 10 | 11 | ### Major Changes 12 | 13 | - 3a7005b: 14 | - Only require magic in `pages/_app`, not in every route file. 15 | - Removed `options.layoutsDir`. `_layout` files must now always be in `pages/`. 16 | 17 | ## 1.0.0 18 | 19 | ### Major Changes 20 | 21 | - Initial release. 22 | 23 | Add `_layout.tsx` files with default exports in your `pages/` directory: 24 | 25 | ``` 26 | pages 27 | ├── dashboard 28 | │   ├── _layout.tsx 29 | │   └── user 30 | │   ├── _layout.tsx 31 | │   └── index.tsx 32 | ├── _layout.tsx 33 | └── index.tsx 34 | ``` 35 | 36 | For example: 37 | 38 | ```javascript 39 | // pages/_layout.tsx 40 | export default function Layout({ children }) { 41 | return ( 42 |
43 |

44 | pages/_layout 45 |

46 | {children} 47 |
48 | ); 49 | } 50 | 51 | // To hide this layout component from the router / build pipeline 52 | export const getStaticProps = async () => ({ notFound: true }); 53 | ``` 54 | 55 | Next, load the layout component with 56 | [`preval`](https://github.com/kentcdodds/babel-plugin-preval) & 57 | [`codegen`](https://github.com/kentcdodds/babel-plugin-codegen): 58 | 59 | ```javascript 60 | // pages/dashboard/user/index.tsx 61 | const filename = preval`module.exports = __filename`; 62 | const Layout = codegen.require("@ceteio/next-layout-loader", filename); 63 | 64 | export default function User() { 65 | return ( 66 | 67 |

Hello world

68 |
69 | ); 70 | } 71 | ``` 72 | 73 | Now, `` is a composition of all `_layout.tsx` files found in the 74 | `pages/` directory from the current file, up to the root (ie; 75 | `pages/dashboard/user/_layout.tsx`, `pages/dashboard/_layout.tsx`, and 76 | `pages/_layout.tsx`). 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cete 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 |
2 |

Next Layout Loader

3 |
4 |
5 |
6 |

7 | File-system based nested layouts for next.js 8 |

9 |

10 | 11 | Try it on Codesandbox 12 | 13 |

14 |
15 |
16 |
17 |
18 | 19 | ``` 20 | yarn add @ceteio/next-layout-loader 21 | ``` 22 | 23 | ## Usage 24 | 25 | Add `_layout.tsx`* files in your `pages/` directory: 26 | 27 | ``` 28 | pages 29 | ├── _app.tsx 30 | ├── _layout.tsx 31 | ├── index.tsx 32 | └── dashboard 33 |    ├── _layout.tsx 34 |    └── user 35 |    ├── _layout.tsx 36 |    └── index.tsx 37 | ``` 38 | 39 | _* (Supports `.tsx`, `.ts`, `.jsx`, `.js`, or [any 40 | custom filename with the `layoutFilenames` option](#optionslayoutfilenames))_ 41 | 42 | For example: 43 | 44 | ```javascript 45 | // pages/_layout.tsx 46 | import { useState } from "react"; 47 | 48 | // children is the file-system based component as rendered by next.js 49 | export default function Layout({ children }) { 50 | // State is maintained between client-side route changes! 51 | const [count, setCount] = useState(0); 52 | return ( 53 |
54 |

55 | pages/_layout 56 | 57 |

58 | {children} 59 |
60 | ); 61 | } 62 | 63 | // To hide this layout component from the router / build pipeline 64 | export const getStaticProps = async () => ({ notFound: true }); 65 | ``` 66 | 67 | Next, add some one-time boilerplate to `_app` (_powered by 68 | [`preval`](https://github.com/kentcdodds/babel-plugin-preval) & 69 | [`codegen`](https://github.com/kentcdodds/babel-plugin-codegen)_): 70 | 71 | 72 | ```javascript 73 | // pages/_app.jsx 74 | const filename = preval`module.exports = __filename`; 75 | const withLayoutLoader = codegen.require("@ceteio/next-layout-loader", filename); 76 | 77 | // Automatically renders _layout files appropriate for the current route 78 | export default withLayoutLoader(({ Component, pageProps }) => ( 79 | 80 | )); 81 | ``` 82 | 83 | Now load your pages to see the layouts automatically applied! 84 | 85 | ## Setup 86 | 87 | Install all the dependencies: 88 | 89 | ``` 90 | yarn add @ceteio/next-layout-loader 91 | yarn add babel-plugin-codegen@4.1.5 babel-plugin-preval 92 | yarn add patch-package postinstall-postinstall 93 | ``` 94 | 95 | The usage of [`preval`](https://github.com/kentcdodds/babel-plugin-preval) & 96 | [`codegen`](https://github.com/kentcdodds/babel-plugin-codegen) necessitates 97 | using `babel`, and hence opting-out of `swc` _(if you know how to do codegen in 98 | `swc`, please let me know in 99 | [#1](https://github.com/ceteio/next-layout-loader/issues/1)!)_. To ensure the 100 | layout files are loaded correctly, you must include the `codegen` and `preval` 101 | plugins: 102 | 103 | `.babelrc` 104 | 105 | ```json 106 | { 107 | "presets": ["next/babel"], 108 | "plugins": ["codegen", "preval"] 109 | } 110 | ``` 111 | 112 | A patch is necessary for `babel-plugin-codegen` to correctly import the 113 | `@ceteio/next-layout-loader` module: 114 | 115 | `package.json` 116 | 117 | ```json 118 | { 119 | "scripts": { 120 | "postinstall": "patch-package" 121 | } 122 | } 123 | ``` 124 | 125 | And create a new file `patches/babel-plugin-codegen+4.1.5.patch`: 126 | 127 | ``` 128 | diff --git a/node_modules/babel-plugin-codegen/dist/helpers.js b/node_modules/babel-plugin-codegen/dist/helpers.js 129 | index e292c8a..472d128 100644 130 | --- a/node_modules/babel-plugin-codegen/dist/helpers.js 131 | +++ b/node_modules/babel-plugin-codegen/dist/helpers.js 132 | @@ -99,9 +99,8 @@ function resolveModuleContents({ 133 | filename, 134 | module 135 | }) { 136 | - const resolvedPath = _path.default.resolve(_path.default.dirname(filename), module); 137 | - 138 | - const code = _fs.default.readFileSync(require.resolve(resolvedPath)); 139 | + const resolvedPath = require.resolve(module, { paths: [_path.default.dirname(filename)] }) 140 | + const code = _fs.default.readFileSync(resolvedPath); 141 | 142 | return { 143 | code, 144 | ``` 145 | 146 | Then re-run `yarn`. 147 | 148 | ## Configuration 149 | 150 | ``` 151 | codegen.require("@ceteio/next-layout-loader", [, options]) 152 | ``` 153 | 154 | ### `` 155 | 156 | Absolute path to the current page file. 157 | 158 | In the simplest case, this can be hard-coded, but wouldn't work on a different 159 | computer, or if you were to move your source files around. Instead, we use 160 | `preval` & `__filename` to automatically generate the correct path for us: 161 | 162 | 163 | ```javascript 164 | const filename = preval`module.exports = __filename`; 165 | const withLayoutLoader = codegen.require("@ceteio/next-layout-loader", filename); 166 | ``` 167 | 168 | _(NOTE: This must remain as 2 separate lines. If you know how to minimise this 169 | boilerplate, please see 170 | [#2](https://github.com/ceteio/next-layout-loader/issues/2)_). 171 | 172 | ### `options` 173 | 174 | An object of further options to affect how the library loads layout files. 175 | 176 | ```javascript 177 | codegen.require("@ceteio/next-layout-loader", filename, { 178 | layoutFilenames 179 | }); 180 | ``` 181 | 182 | #### `options.layoutFilenames` 183 | 184 | _Default_: `['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js']` 185 | 186 | The possible variations of layout file names within `pages/`. Can be overridden 187 | to use any name or extension you like. 188 | 189 | ## How it works 190 | 191 | The easiest way to understand with an example: 192 | 193 | ``` 194 | pages 195 | ├── index.tsx 196 | ├── _app.tsx 197 | ├── _layout.tsx 198 | └── dashboard 199 |    ├── _layout.tsx 200 |    └── user 201 |    ├── index.tsx 202 |    └── _layout.tsx 203 | ``` 204 | 205 | `pages/_app.tsx`: 206 | 207 | ```javascript 208 | const filename = preval`module.exports = __filename`; 209 | const withLayoutLoader = codegen.require( 210 | "@ceteio/next-layout-loader", 211 | filename 212 | ); 213 | 214 | // Automatically renders _layout files appropriate for the current route 215 | export default withLayoutLoader(({ Component, pageProps }) => ( 216 | 217 | )); 218 | ``` 219 | 220 | `pages/dashboard/user/index.tsx`: 221 | 222 | ```javascript 223 | export default function User() { 224 | return

Hello world

; 225 | } 226 | ``` 227 | 228 | `next-layout-loader` will transform the `pages/app.tsx` into: 229 | 230 | ```javascript 231 | import dynamic from "next/dynamic"; 232 | import { Fragment } from "react"; 233 | 234 | // A map of directories to their layout components (if they exist) 235 | const layoutMap = { 236 | "/": __dynamic(() => import("./_layout.jsx")), 237 | dashboard: __dynamic(() => import("./dashboard/_layout.jsx")), 238 | "dashboard/user": __dynamic(() => import("./dashboard/user/_layout.jsx")) 239 | }; 240 | 241 | const withLayoutLoader = wrappedFn => context => { 242 | const { pageProps, router } = context; 243 | 244 | const renderedComponent = wrappedFn(context); 245 | 246 | return ({ Component, pageProps, router }) => { 247 | const Layout1 = layoutMap["/"]; 248 | const Layout2 = layoutMap["dashboard"]; 249 | const Layout3 = layoutMap["dashboard/user"]; 250 | 251 | return ( 252 | 253 | 254 | 255 | {renderedComponent} 256 | 257 | 258 | 259 | ); 260 | }; 261 | })(); 262 | 263 | export default withLayoutLoader(({ Component, pageProps }) => ( 264 | 265 | )); 266 | ``` 267 | 268 | _(Note: The above is a simplification; the real code has some extra logic to 269 | handle all routes and their layouts)_ 270 | 271 | ## Frequently Asked Questions 272 | 273 | ### Why does this exist? 274 | 275 | This library started as Proof Of Concept based on [a 276 | discussion](https://github.com/vercel/next.js/discussions/26389#discussioncomment-922493) 277 | in the Next.js repo, but it turned out to work quite well and match my mental 278 | model of how nested layouts should work. So I turned it into a library that 279 | anyone can use. 280 | 281 | ### Why is an extra layout being applied? 282 | 283 | An extra layout component can be unexpectedly rendered when you have the 284 | following situation: 285 | 286 | ``` 287 | pages 288 | ├── _layout.tsx 289 | ├── user.tsx 290 | └── user 291 | └── _layout.tsx 292 | ``` 293 | 294 | Visiting `/user` may will render both `pages/_layout.tsx` _and_ 295 | `pages/user/_layout.tsx`. This may not be expected (the later is in a child 296 | directory after all!), and is due to a difference in the way Next.js handles 297 | rendering pages vs how `@ceteio/next-layout-loader` loads layouts. 298 | 299 | To work around this, move `pages/user.tsx` to `pages/user/index.tsx`: 300 | 301 | ```diff 302 | pages 303 | ├── _layout.tsx 304 | -├── user.tsx 305 | └── user 306 | + ├── index.tsx 307 | └── _layout.tsx 308 | ``` 309 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | "codegen", 7 | "preval" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a 2 | [`@ceteio/next-layout-loader`](https://github.com/ceteio/next-layout-loader) 3 | example showing automatic loading of `_layout` files from within the `pages/` 4 | directory. 5 | 6 | Use the nav at the top (rendered in `pages/_layout.jsx`) to browser around. Each 7 | layout file specifies its own background color. 8 | 9 | Layouts also maintain their state across client-side navigations. Click the 10 | "count" button, then navigate to a new URL using one of the links to see it in 11 | action. 12 | 13 | ## Getting Started 14 | 15 | First, install the dependencies: 16 | 17 | ```bash 18 | yarn 19 | ``` 20 | 21 | Run the development server: 22 | 23 | ```bash 24 | yarn dev 25 | ``` 26 | 27 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 28 | -------------------------------------------------------------------------------- /example/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-layout-loader-example", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint", 9 | "postinstall": "patch-package" 10 | }, 11 | "dependencies": { 12 | "@ceteio/next-layout-loader": "^2.0.0", 13 | "next": "12.0.7", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2" 16 | }, 17 | "devDependencies": { 18 | "babel-plugin-codegen": "4.1.5", 19 | "babel-plugin-preval": "^5.0.0", 20 | "eslint": "8.5.0", 21 | "eslint-config-next": "12.0.7", 22 | "patch-package": "^6.4.7" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/pages/_app.jsx: -------------------------------------------------------------------------------- 1 | const filename = preval`module.exports = __filename`; 2 | const withLayoutLoader = codegen.require( 3 | "@ceteio/next-layout-loader", 4 | filename 5 | ); 6 | 7 | export default withLayoutLoader(({ Component, pageProps }) => ( 8 | 9 | )); 10 | -------------------------------------------------------------------------------- /example/pages/_layout.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import NextLink from "next/link"; 3 | 4 | export default function Layout({ children }) { 5 | const [count, setCount] = useState(0); 6 | return ( 7 |
8 |

9 | pages/_layout 10 |

    11 |
  • 12 | / 13 |
  • 14 |
  • 15 | /dashboard/user 16 |
  • 17 |
  • 18 | /dashboard/user/123 19 |
  • 20 |
21 | 22 |

23 | {children} 24 |
25 | ); 26 | } 27 | 28 | // To hide this layout component from the router / build pipeline 29 | export const getStaticProps = async () => ({ notFound: true }); 30 | -------------------------------------------------------------------------------- /example/pages/dashboard/_layout.jsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }) { 2 | return ( 3 |
4 |

5 | pages/dashboard/_layout 6 |

7 | {children} 8 |
9 | ); 10 | } 11 | 12 | // To hide this layout component from the router / build pipeline 13 | export const getStaticProps = async () => ({ notFound: true }); 14 | -------------------------------------------------------------------------------- /example/pages/dashboard/user/[id].jsx: -------------------------------------------------------------------------------- 1 | import NextLink from "next/link"; 2 | 3 | export default function User({ userId }) { 4 | return ( 5 |
6 | [User {userId}'s profile] 7 |
8 | ); 9 | } 10 | 11 | export async function getStaticProps({ params }) { 12 | return { 13 | props: { 14 | userId: params.id, 15 | greeting: `Hello ${params.id}` 16 | } 17 | }; 18 | } 19 | 20 | export async function getStaticPaths() { 21 | return { 22 | fallback: true, 23 | paths: [] 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /example/pages/dashboard/user/_layout.jsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ greeting, children }) { 2 | return ( 3 |
4 |

5 | pages/dashboard/user/_layout 6 |

7 |

{greeting}

8 | {children} 9 |
10 | ); 11 | } 12 | 13 | // To hide this layout component from the router / build pipeline 14 | export const getStaticProps = async () => ({ notFound: true }); 15 | -------------------------------------------------------------------------------- /example/pages/dashboard/user/index.jsx: -------------------------------------------------------------------------------- 1 | import NextLink from "next/link"; 2 | 3 | export default function User() { 4 | return ( 5 |
6 | [List of user profiles] 7 |
8 | ); 9 | } 10 | 11 | export async function getStaticProps() { 12 | return { 13 | props: { 14 | greeting: "hello user/index.jsx" 15 | } // will be passed to the page component as props 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /example/pages/index.jsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return ( 3 |
4 |

5 | @ceteio/next-layout-loader Example 6 |

7 |

8 | This example shows{" "} 9 | 10 | 11 | @ceteio/next-layout-loader 12 | 13 | {" "} 14 | automatically loading _layout files from within the{" "} 15 | pages/ directory. 16 |

17 |

18 | Use the nav at the top (rendered in pages/_layout.jsx) to 19 | browser around. Each layout file specifies its own background color. 20 |

21 |

22 | Layouts also maintain their state across client-side navigations. Click 23 | the "count" button, then navigate to a new URL using one of the links to 24 | see it in action. 25 |

26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /example/patches/babel-plugin-codegen+4.1.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/babel-plugin-codegen/dist/helpers.js b/node_modules/babel-plugin-codegen/dist/helpers.js 2 | index e292c8a..472d128 100644 3 | --- a/node_modules/babel-plugin-codegen/dist/helpers.js 4 | +++ b/node_modules/babel-plugin-codegen/dist/helpers.js 5 | @@ -99,9 +99,8 @@ function resolveModuleContents({ 6 | filename, 7 | module 8 | }) { 9 | - const resolvedPath = _path.default.resolve(_path.default.dirname(filename), module); 10 | - 11 | - const code = _fs.default.readFileSync(require.resolve(resolvedPath)); 12 | + const resolvedPath = require.resolve(module, { paths: [_path.default.dirname(filename)] }) 13 | + const code = _fs.default.readFileSync(resolvedPath); 14 | 15 | return { 16 | code, 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ceteio/next-layout-loader", 3 | "version": "2.0.1", 4 | "description": "Automatic next.js layout component loader", 5 | "main": "dist/ceteio-next-layout-loader.cjs.js", 6 | "module": "dist/ceteio-next-layout-loader.esm.js", 7 | "author": "Jess Telford ", 8 | "license": "MIT", 9 | "files": [ 10 | "dist", 11 | "CHANGELOG.md", 12 | "package.json", 13 | "README.md", 14 | "LICENSE" 15 | ], 16 | "publishConfig": { 17 | "access": "public", 18 | "registry": "https://registry.npmjs.org" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/ceteio/next-layout-loader.git" 23 | }, 24 | "scripts": { 25 | "build": "preconstruct build", 26 | "release": "yarn build && changeset version && echo \"Note the new version in package.json, revert the change there, commit the changelog, then run 'yarn np'\"" 27 | }, 28 | "preconstruct": { 29 | "entrypoints": [ 30 | "index.js" 31 | ] 32 | }, 33 | "dependencies": { 34 | "@babel/runtime": "^7.15.4" 35 | }, 36 | "devDependencies": { 37 | "@babel/preset-env": "^7.16.5", 38 | "@changesets/cli": "^2.18.1", 39 | "@preconstruct/cli": "^2.1.5", 40 | "babel-plugin-codegen": "^4.1.5", 41 | "babel-plugin-preval": "^5.0.0", 42 | "np": "^7.6.0" 43 | }, 44 | "peerDependencies": { 45 | "babel-plugin-codegen": "^4.1.5", 46 | "babel-plugin-preval": "^5.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = ( 2 | appFilename, 3 | { 4 | layoutFilenames = ["_layout.tsx", "_layout.ts", "_layout.jsx", "_layout.js"] 5 | } = {} 6 | ) => { 7 | const path = require("path"); 8 | const fs = require("fs"); 9 | 10 | // Figure out where the `pages/` directory is, as an absolute path 11 | const pagesDir = path.dirname(appFilename); 12 | 13 | const collectLayouts = (parentDir, filename, result) => { 14 | const entryPath = path.join(parentDir, filename); 15 | const entry = fs.lstatSync(entryPath); 16 | if (entry.isDirectory()) { 17 | fs.readdirSync(entryPath, { 18 | withFileTypes: true 19 | }).forEach(childEntry => 20 | collectLayouts(entryPath, childEntry.name, result) 21 | ); 22 | } else if (layoutFilenames.includes(filename)) { 23 | result.push(entryPath); 24 | } 25 | }; 26 | 27 | const layoutFiles = []; 28 | collectLayouts(path.dirname(pagesDir), path.basename(pagesDir), layoutFiles); 29 | 30 | const layoutMap = layoutFiles.reduce((memo, layoutFilePath) => { 31 | const relativePath = path 32 | .relative(pagesDir, layoutFilePath) 33 | // Forcibly convert it to a posix-style path since that's what node 34 | // modules expect, and what Next.js router returns 35 | .split(path.sep) 36 | .join(path.posix.sep); 37 | const key = path.posix.dirname(relativePath).replace(/^\.\//, ""); 38 | memo[key === "." ? "/" : key] = `./${relativePath}`; 39 | return memo; 40 | }, {}); 41 | 42 | // +1 to account for the root `_layout` file 43 | const maxLayoutDepth = 44 | 1 + 45 | Object.keys(layoutMap).reduce( 46 | (maxSoFar, layoutFilePath) => 47 | Math.max( 48 | // Count the number of full path segments. Uses .filter(Boolean) for the 49 | // special case of '/'.split('/') === ['', '']. 50 | layoutFilePath.split(path.sep).filter(Boolean).length, 51 | maxSoFar 52 | ), 53 | 0 54 | ); 55 | 56 | // By returning an IIFE we're able to namespace our variables, and allow the 57 | // callsite to dictacte the export name while keeping linters happy. 58 | return `(() => { 59 | const __dynamic = require("next/dynamic").default; 60 | const { Fragment } = require("react"); 61 | 62 | // A map of directories to their layout components (if they exist) 63 | const layoutMap = { 64 | ${Object.entries(layoutMap) 65 | .map( 66 | ([key, layoutFilePath]) => 67 | `"${key}": __dynamic(() => import("${layoutFilePath}")),` 68 | ) 69 | .join("\n")} 70 | }; 71 | 72 | return wrappedFn => context => { 73 | const { pageProps, router } = context; 74 | 75 | const renderedComponent = wrappedFn(context); 76 | 77 | const Layout0 = layoutMap["/"] || Fragment; 78 | const layout0Props = Layout0 !== Fragment ? pageProps : {}; 79 | 80 | // Special case, because "".split('/').length === 1, which conflicts with 81 | // "dashboard".split('/').length === 1 82 | if (router.route === "/") { 83 | return ( 84 | 85 | {renderedComponent} 86 | 87 | ); 88 | } 89 | 90 | const parts = (router.route[0] === "/" 91 | ? router.route.slice(1) 92 | : router.route 93 | ).split("/"); 94 | 95 | ${Array.apply(null, Array(maxLayoutDepth)) 96 | .map((_, index) => index + 1) 97 | .map( 98 | num => ` 99 | const Layout${num} = (parts.length >= ${num} && layoutMap[parts.slice(0, ${num}).join("/")]) || Fragment; 100 | const layout${num}Props = Layout${num} !== Fragment ? pageProps : {}; 101 | 102 | ` 103 | ) 104 | .join("\n")} 105 | 106 | return ( 107 | ${Array.apply(null, Array(maxLayoutDepth)) 108 | .map((_, index) => index) 109 | .reverse() 110 | .reduce( 111 | (memo, num) => ` 112 | 113 | ${memo} 114 | 115 | `, 116 | "{renderedComponent}" 117 | )} 118 | ); 119 | }; 120 | })();`; 121 | }; 122 | --------------------------------------------------------------------------------