diff --git a/packages/gasket-plugin-nextjs/README.md b/packages/gasket-plugin-nextjs/README.md index a35e082a2..67ab2551a 100644 --- a/packages/gasket-plugin-nextjs/README.md +++ b/packages/gasket-plugin-nextjs/README.md @@ -33,7 +33,7 @@ module.exports = { ## Adding a Sitemap -When creating a new application with this plugin, you will be prompted with a question in the CLI asking if you would like to add a [sitemap] to your application. +When creating a new application with this plugin, you will be prompted with a question in the CLI asking if you would like to add a [sitemap] to your application. Answering yes to this question will install `next-sitemap` as a dependency, generate a next-sitemap.config.js file, and add a `sitemap` npm script to your package.json. `next-sitemap` is an npm package that generates sitemaps and a robots.txt file for Next.js applications. Learn more by reading the [next-sitemap docs]. @@ -233,7 +233,7 @@ module.exports = { ## Utilities -This plugin adds a middleware which attaches a `getNextRoute` function to the request object. It is intended for use in server contexts where you need to know how a request will route to a next.js page. This async function returns null if the manifest could not be parsed or if the requested URL does not match a route. If a match _is_ found, an object with these properties is returned: +This plugin hooks the `actions` lifecycle creates a `getNextRoute` action. It is intended for use in server contexts where you need to know how a request will route to a next.js page. This async function returns null if the manifest could not be parsed or if the requested URL does not match a route. If a match _is_ found, an object with these properties is returned: | Property | Type | Description | |----------|------|-------------| @@ -249,7 +249,7 @@ async function someMiddleware(req, res, next) { const { groups } = req.url.match(route.namedRegex); console.log(`Matched ${route.page} with parameters`, groups); } - + next(); } ``` diff --git a/packages/gasket-plugin-nextjs/generator/app/app-router/app/page.js b/packages/gasket-plugin-nextjs/generator/app/app-router/app/page.js index 0a2c10488..ae74b83fc 100644 --- a/packages/gasket-plugin-nextjs/generator/app/app-router/app/page.js +++ b/packages/gasket-plugin-nextjs/generator/app/app-router/app/page.js @@ -6,4 +6,4 @@ export const metadata = { charset: 'UTF-8' }; -export default IndexPage +export default IndexPage; diff --git a/packages/gasket-plugin-nextjs/lib/actions.js b/packages/gasket-plugin-nextjs/lib/actions.js new file mode 100644 index 000000000..a603bc8d2 --- /dev/null +++ b/packages/gasket-plugin-nextjs/lib/actions.js @@ -0,0 +1,22 @@ +const { createConfig } = require('./utils/config'); +const nextRoute = require('./utils/next-route'); + +/** @type {import('@gasket/core').HookHandler<'actions'>} */ +module.exports = function actions(gasket) { + return { + getNextConfig(nextConfig) { + return async function setupNextConfig(phase, { defaultConfig }) { + let baseConfig; + if (nextConfig instanceof Function) { + baseConfig = await nextConfig(phase, { defaultConfig }); + } else { + baseConfig = nextConfig ?? {}; + } + return createConfig(gasket, phase === 'phase-production-build', baseConfig); + }; + }, + async getNextRoute(req) { + return await nextRoute(gasket, req); + } + }; +}; diff --git a/packages/gasket-plugin-nextjs/lib/apm-transaction.js b/packages/gasket-plugin-nextjs/lib/apm-transaction.js index 40aa1e133..31d0e891c 100644 --- a/packages/gasket-plugin-nextjs/lib/apm-transaction.js +++ b/packages/gasket-plugin-nextjs/lib/apm-transaction.js @@ -2,7 +2,7 @@ /** @type {import('@gasket/core').HookHandler<'apmTransaction'>} */ module.exports = async function apmTransaction(gasket, transaction, { req }) { - const route = await req.getNextRoute(); + const route = await gasket.actions.getNextRoute(req); if (!route) { return; diff --git a/packages/gasket-plugin-nextjs/lib/create.js b/packages/gasket-plugin-nextjs/lib/create.js index 3b5d15508..219a4839d 100644 --- a/packages/gasket-plugin-nextjs/lib/create.js +++ b/packages/gasket-plugin-nextjs/lib/create.js @@ -119,10 +119,9 @@ function addNpmScripts({ pkg, nextServerType, nextDevProxy, typescript }) { const fileExtension = typescript ? 'ts' : 'js'; const scripts = { defaultServer: { - 'build': 'next build', - 'start': 'next start', - 'start:local': `next start & GASKET_ENV=local node server.${fileExtension}`, - 'local': `next dev${nextDevProxy ? ` & nodemon server.${fileExtension}` : ''}` + build: 'next build', + start: 'next start', + local: `next dev${nextDevProxy ? ` & nodemon server.${fileExtension}` : ''}` }, customServer: { 'build': 'next build', @@ -132,6 +131,10 @@ function addNpmScripts({ pkg, nextServerType, nextDevProxy, typescript }) { } }; + if (nextDevProxy && nextServerType === 'defaultServer') { + scripts.defaultServer['start:local'] = `next start & GASKET_ENV=local node server.${fileExtension}`; + } + pkg.add('scripts', scripts[nextServerType]); } @@ -154,7 +157,6 @@ function addConfig(createContext) { } } - module.exports = { timing: { before: ['@gasket/plugin-intl'], diff --git a/packages/gasket-plugin-nextjs/lib/index.d.ts b/packages/gasket-plugin-nextjs/lib/index.d.ts index 7eff75d70..b9177d106 100644 --- a/packages/gasket-plugin-nextjs/lib/index.d.ts +++ b/packages/gasket-plugin-nextjs/lib/index.d.ts @@ -17,6 +17,12 @@ declare module '@gasket/core' { export interface GasketActions { getNextConfig?: (config?: NextConfig | NextConfigFunction) => (phase: string, context?: { defaultConfig?: any }) => Promise + getNextRoute?: (req: IncomingMessage) => Promise; + namedRegex: RegExp; + }>; } export interface GasketConfig { @@ -60,12 +66,6 @@ declare module '@gasket/plugin-webpack' { declare module 'http' { export interface IncomingMessage { - getNextRoute(): Promise; - namedRegex: RegExp; - }>; path?: string; } } diff --git a/packages/gasket-plugin-nextjs/lib/index.js b/packages/gasket-plugin-nextjs/lib/index.js index 16ad0a90e..5e981cf82 100644 --- a/packages/gasket-plugin-nextjs/lib/index.js +++ b/packages/gasket-plugin-nextjs/lib/index.js @@ -1,12 +1,13 @@ +/// + const { name, version, description } = require('../package.json'); const apmTransaction = require('./apm-transaction'); const metadata = require('./metadata'); const configure = require('./configure'); -const { createConfig } = require('./utils/config'); +const actions = require('./actions'); const prompt = require('./prompt'); const create = require('./create'); const { webpackConfig } = require('./webpack-config'); -const middleware = require('./middleware'); const express = require('./express'); const fastify = require('./fastify'); const workbox = require('./workbox'); @@ -20,24 +21,9 @@ const plugin = { hooks: { configure, webpackConfig, - actions(gasket) { - return { - getNextConfig(nextConfig) { - return async function setupNextConfig(phase, { defaultConfig }) { - let baseConfig; - if (nextConfig instanceof Function) { - baseConfig = await nextConfig(phase, { defaultConfig }); - } else { - baseConfig = nextConfig ?? {}; - } - return createConfig(gasket, phase === 'phase-production-build', baseConfig); - }; - } - }; - }, + actions, prompt, create, - middleware, express, fastify, apmTransaction, diff --git a/packages/gasket-plugin-nextjs/lib/middleware.js b/packages/gasket-plugin-nextjs/lib/middleware.js deleted file mode 100644 index cfdcda084..000000000 --- a/packages/gasket-plugin-nextjs/lib/middleware.js +++ /dev/null @@ -1,16 +0,0 @@ -/// - -const getNextRoute = require('./utils/next-route'); - -module.exports = { - timing: { - before: ['@gasket/plugin-elastic-apm'] - }, - /** @type {import('@gasket/core').HookHandler<'middleware'>} */ - handler: (gasket) => [ - (req, _res, next) => { - req.getNextRoute = () => getNextRoute(gasket, req); - next(); - } - ] -}; diff --git a/packages/gasket-plugin-nextjs/test/actions.test.js b/packages/gasket-plugin-nextjs/test/actions.test.js new file mode 100644 index 000000000..887bac0d7 --- /dev/null +++ b/packages/gasket-plugin-nextjs/test/actions.test.js @@ -0,0 +1,10 @@ +const actions = require('../lib/actions'); + +describe('actions', () => { + + it('has expected actions', () => { + const results = actions({}); + expect(results).toHaveProperty('getNextConfig'); + expect(results).toHaveProperty('getNextRoute'); + }); +}); diff --git a/packages/gasket-plugin-nextjs/test/apm-transaction.test.js b/packages/gasket-plugin-nextjs/test/apm-transaction.test.js index 636d42239..ee0647d87 100644 --- a/packages/gasket-plugin-nextjs/test/apm-transaction.test.js +++ b/packages/gasket-plugin-nextjs/test/apm-transaction.test.js @@ -1,84 +1,85 @@ const apmTransaction = require('../lib/apm-transaction'); describe('The apmTransaction hook', () => { - let transaction; + let transaction, mockGasket; beforeEach(() => { transaction = { name: 'GET *', addLabels: jest.fn() }; + + mockGasket = { + actions: { + getNextRoute: jest.fn() + } + }; }); it('retains the original name if the route is not for a page', async () => { transaction.name = 'GET /proxy/foo'; - const req = { getNextRoute: async () => null }; - - await apmTransaction({}, transaction, { req }); - + await apmTransaction(mockGasket, transaction, { req: {} }); expect(transaction.name).toEqual('GET /proxy/foo'); }); it('returns the page name if the route is for a page', async () => { + mockGasket.actions.getNextRoute.mockResolvedValueOnce({ + page: '/customer/[id]', + namedRegex: /^\/customer\/(?[^/]+)(?:\/)?$/ + }); const req = { - url: '/customer/123', - getNextRoute: async () => ({ - page: '/customer/[id]', - namedRegex: /^\/customer\/(?[^/]+)(?:\/)?$/ - }) + url: '/customer/123' }; - await apmTransaction({}, transaction, { req }); + await apmTransaction(mockGasket, transaction, { req }); expect(transaction.name).toEqual('/customer/[id]'); }); it('does not set labels if the route is not for a page', async () => { - const req = { getNextRoute: async () => null }; - - await apmTransaction({}, transaction, { req }); + await apmTransaction(mockGasket, transaction, { req: {} }); expect(transaction.addLabels).not.toHaveBeenCalled(); }); it('sets dynamic route params as labels if the route is for a page', async () => { + mockGasket.actions.getNextRoute.mockResolvedValueOnce({ + page: '/cohorts/[cohortId]', + namedRegex: /^\/cohorts\/(?[^/]+)(?:\/)?$/ + }); const req = { - url: '/cohorts/Rad%20People', - getNextRoute: async () => ({ - page: '/cohorts/[cohortId]', - namedRegex: /^\/cohorts\/(?[^/]+)(?:\/)?$/ - }) + url: '/cohorts/Rad%20People' }; - await apmTransaction({}, transaction, { req }); + await apmTransaction(mockGasket, transaction, { req }); expect(transaction.addLabels).toHaveBeenCalledWith({ cohortId: 'Rad People' }); }); it('handles URL segments that are not properly URL encoded', async () => { + mockGasket.actions.getNextRoute.mockResolvedValueOnce({ + page: '/cohorts/[cohortId]', + namedRegex: /^\/cohorts\/(?[^/]+)(?:\/)?$/ + }); const req = { - url: '/cohorts/TopTen%', - getNextRoute: async () => ({ - page: '/cohorts/[cohortId]', - namedRegex: /^\/cohorts\/(?[^/]+)(?:\/)?$/ - }) + url: '/cohorts/TopTen%' }; - await apmTransaction({}, transaction, { req }); + await apmTransaction(mockGasket, transaction, { req }); expect(transaction.addLabels).toHaveBeenCalledWith({ cohortId: 'TopTen%' }); }); it('ignores the query string when parsing the route', async () => { + mockGasket.actions.getNextRoute.mockResolvedValueOnce({ + page: '/cohorts/[cohortId]', + namedRegex: /^\/cohorts\/(?[^/]+)(?:\/)?$/ + }); const req = { - url: '/cohorts/Rad%20People?utm_source=TDFS_DASLNC&utm_medium=parkedpages&utm_campaign=x_corp_tdfs-daslnc_base&traffic_type=TDFS_DASLNC&traffic_id=daslnc&sayfa=20&act=ul&listurun=12&sublastcat=&subcat=61&marka=&sort=2&catname=Realistik%20Belden%20Ba%F0lamal%FD', - getNextRoute: async () => ({ - page: '/cohorts/[cohortId]', - namedRegex: /^\/cohorts\/(?[^/]+)(?:\/)?$/ - }) + url: '/cohorts/Rad%20People?utm_source=TDFS_DASLNC&utm_medium=parkedpages&utm_campaign=x_corp_tdfs-daslnc_base&traffic_type=TDFS_DASLNC&traffic_id=daslnc&sayfa=20&act=ul&listurun=12&sublastcat=&subcat=61&marka=&sort=2&catname=Realistik%20Belden%20Ba%F0lamal%FD' }; - await apmTransaction({}, transaction, { req }); + await apmTransaction(mockGasket, transaction, { req }); expect(transaction.addLabels).toHaveBeenCalledWith({ cohortId: 'Rad People' }); }); diff --git a/packages/gasket-plugin-nextjs/test/create.test.js b/packages/gasket-plugin-nextjs/test/create.test.js index 618fb45a4..c3f910ee5 100644 --- a/packages/gasket-plugin-nextjs/test/create.test.js +++ b/packages/gasket-plugin-nextjs/test/create.test.js @@ -171,13 +171,24 @@ describe('create hook', () => { await plugin.hooks.create.handler({}, mockContext); expect(mockContext.pkg.add).toHaveBeenCalledWith('scripts', { - 'build': 'next build', - 'start': 'next start', - 'start:local': 'next start & GASKET_ENV=local node server.js', - 'local': 'next dev' + build: 'next build', + start: 'next start', + local: 'next dev' }); }); + it('adds start:local script for next cli w/devProxy', async function () { + mockContext.nextServerType = 'defaultServer'; + mockContext.nextDevProxy = true; + await plugin.hooks.create.handler({}, mockContext); + + expect(mockContext.pkg.add).toHaveBeenCalledWith('scripts', + expect.objectContaining({ + 'start:local': 'next start & GASKET_ENV=local node server.js' + }) + ); + }); + it('adds the appropriate npm scripts for next custom server', async function () { mockContext.nextServerType = 'customServer'; await plugin.hooks.create.handler({}, mockContext); diff --git a/packages/gasket-plugin-nextjs/test/index.test.js b/packages/gasket-plugin-nextjs/test/index.test.js index 72b602b02..90af3aed9 100644 --- a/packages/gasket-plugin-nextjs/test/index.test.js +++ b/packages/gasket-plugin-nextjs/test/index.test.js @@ -55,7 +55,6 @@ describe('Plugin', function () { 'express', 'fastify', 'metadata', - 'middleware', 'prompt', 'webpackConfig', 'workbox'