├── .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 | setCount(count + 1)}>Count: {count}
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 |
setCount(count + 1)}>Count: {count}
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 |
--------------------------------------------------------------------------------