From 7c98a4112093d82c77bdbe738e83f691c658fbf9 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Tue, 7 May 2024 19:39:47 +0100 Subject: [PATCH] feat(cli): Setup command and codemod for OG image middleware (#10485) This PR introduces a setup command for the OG image generation middleware. --- It did involve moving around some of the codemod utilities we have. Either because we needed to include them where we previously didn't or because they no longer made sense in the directory they were in. --- .../__tests__/fragmentsHandler.test.ts | 2 +- .../features/fragments/fragmentsHandler.ts | 3 +- .../__tests__/trustedDocuments.test.ts | 4 +- .../trustedDocumentsHandler.ts | 2 +- .../commands/setup/middleware/middleware.ts | 17 +++ .../ogImage/__codemod_tests__/middleware.ts | 18 +++ .../ogImage/__codemod_tests__/vitePlugin.ts | 7 + .../alreadyContainsImport.input.tsx | 18 +++ .../alreadyContainsImport.output.tsx | 27 ++++ .../__testfixtures__/defaultTsx.input.tsx | 17 +++ .../__testfixtures__/defaultTsx.output.tsx | 27 ++++ .../defaultViteConfig.input.tsx | 19 +++ .../defaultViteConfig.output.tsx | 20 +++ .../registerFunctionAlreadyDefined.input.tsx | 27 ++++ .../registerFunctionAlreadyDefined.output.tsx | 33 +++++ .../middleware/ogImage/codemodMiddleware.ts | 129 ++++++++++++++++++ .../middleware/ogImage/codemodVitePlugin.ts | 91 ++++++++++++ .../setup/middleware/ogImage/ogImage.ts | 26 ++++ .../middleware/ogImage/ogImageHandler.ts | 125 +++++++++++++++++ .../fragments => lib}/runTransform.ts | 0 .../cli/src/testUtils/matchFolderTransform.ts | 2 +- .../testUtils/matchInlineTransformSnapshot.ts | 4 +- .../src/testUtils/matchTransformSnapshot.ts | 87 ++++++++++++ packages/cli/testUtils.d.ts | 7 + packages/cli/vitest.codemods.setup.ts | 3 + .../src/testUtils/matchTransformSnapshot.ts | 6 +- 26 files changed, 713 insertions(+), 8 deletions(-) create mode 100644 packages/cli/src/commands/setup/middleware/middleware.ts create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/middleware.ts create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/vitePlugin.ts create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.input.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.output.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.input.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.output.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.input.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.output.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.input.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.output.tsx create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/codemodMiddleware.ts create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/codemodVitePlugin.ts create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/ogImage.ts create mode 100644 packages/cli/src/commands/setup/middleware/ogImage/ogImageHandler.ts rename packages/cli/src/{commands/setup/graphql/features/fragments => lib}/runTransform.ts (100%) create mode 100644 packages/cli/src/testUtils/matchTransformSnapshot.ts diff --git a/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts b/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts index 413b95f277b7..b05fad40b57f 100644 --- a/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts +++ b/packages/cli/src/commands/setup/graphql/features/fragments/__tests__/fragmentsHandler.test.ts @@ -21,7 +21,7 @@ vi.mock('node:fs', async () => { }) vi.mock('execa') // The jscodeshift parts are tested by another test -vi.mock('../runTransform', () => { +vi.mock('../../../../../../lib/runTransform', () => { return { runTransform: () => { return {} diff --git a/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts b/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts index 160d51ee2802..061f8b8b546e 100644 --- a/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts +++ b/packages/cli/src/commands/setup/graphql/features/fragments/fragmentsHandler.ts @@ -12,8 +12,9 @@ import { } from '@redwoodjs/cli-helpers' import { getConfig, getPaths } from '@redwoodjs/project-config' +import { runTransform } from '../../../../../lib/runTransform' + import type { Args } from './fragments' -import { runTransform } from './runTransform' export const command = 'fragments' export const description = 'Set up Fragments for GraphQL' diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts index c1182342f560..9833d626734c 100644 --- a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/__tests__/trustedDocuments.test.ts @@ -5,7 +5,9 @@ vi.mock('fs', async () => ({ ...memfsFs, default: { ...memfsFs } })) vi.mock('node:fs', async () => ({ ...memfsFs, default: { ...memfsFs } })) vi.mock('execa') // The jscodeshift parts are tested by another test -vi.mock('../../fragments/runTransform', () => ({ runTransform: () => ({}) })) +vi.mock('../../../../../../lib/runTransform', () => ({ + runTransform: () => ({}), +})) vi.mock('listr2', () => { return { diff --git a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts index b9a7616ac865..8049052dfa64 100644 --- a/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts +++ b/packages/cli/src/commands/setup/graphql/features/trustedDocuments/trustedDocumentsHandler.ts @@ -8,7 +8,7 @@ import { format } from 'prettier' import { getPrettierOptions, setTomlSetting } from '@redwoodjs/cli-helpers' import { getConfig, getPaths, resolveFile } from '@redwoodjs/project-config' -import { runTransform } from '../fragments/runTransform.js' +import { runTransform } from '../../../../../lib/runTransform' export async function handler({ force }: { force: boolean }) { const tasks = new Listr( diff --git a/packages/cli/src/commands/setup/middleware/middleware.ts b/packages/cli/src/commands/setup/middleware/middleware.ts new file mode 100644 index 000000000000..5fb7d4c180e4 --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/middleware.ts @@ -0,0 +1,17 @@ +import terminalLink from 'terminal-link' +import type { Argv } from 'yargs' + +import * as ogImageCommand from './ogImage/ogImage' + +export const command = 'middleware ' +export const description = 'Set up a middleware' +export function builder(yargs: Argv) { + return yargs + .command(ogImageCommand) + .epilogue( + `Also see the ${terminalLink( + 'Redwood CLI Reference', + 'https://redwoodjs.com/docs/cli-commands', + )}`, + ) +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/middleware.ts b/packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/middleware.ts new file mode 100644 index 000000000000..ec26ffa8493e --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/middleware.ts @@ -0,0 +1,18 @@ +import { describe, it } from 'vitest' + +describe('Middleware codemod', () => { + it('Handles the default TSX case', async () => { + await matchTransformSnapshot('codemodMiddleware', 'defaultTsx') + }) + + it('Handles when OgImageMiddleware is already imported', async () => { + await matchTransformSnapshot('codemodMiddleware', 'alreadyContainsImport') + }) + + it('Handles when registerMiddleware function is already defined', async () => { + await matchTransformSnapshot( + 'codemodMiddleware', + 'registerFunctionAlreadyDefined', + ) + }) +}) diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/vitePlugin.ts b/packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/vitePlugin.ts new file mode 100644 index 000000000000..2b70ed62c956 --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__codemod_tests__/vitePlugin.ts @@ -0,0 +1,7 @@ +import { describe, it } from 'vitest' + +describe('Vite plugin codemod', () => { + it('Handles the default vite config case', async () => { + await matchTransformSnapshot('codemodVitePlugin', 'defaultViteConfig') + }) +}) diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.input.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.input.tsx new file mode 100644 index 000000000000..8251f4a4c36b --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.input.tsx @@ -0,0 +1,18 @@ +import OgImageMiddleware from "@redwoodjs/ogimage-gen/middleware"; +import type { TagDescriptor } from '@redwoodjs/web' + +import App from './App' +import { Document } from './Document' + +interface Props { + css: string[] + meta?: TagDescriptor[] +} + +export const ServerEntry: React.FC = ({ css, meta }) => { + return ( + + + + ) +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.output.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.output.tsx new file mode 100644 index 000000000000..9fb7d82a95ca --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/alreadyContainsImport.output.tsx @@ -0,0 +1,27 @@ +import OgImageMiddleware from "@redwoodjs/ogimage-gen/middleware"; +import type { TagDescriptor } from '@redwoodjs/web' + +import App from './App' +import { Document } from './Document' + +interface Props { + css: string[] + meta?: TagDescriptor[] +} + +export const ServerEntry: React.FC = ({ css, meta }) => { + return ( + + + + ) +} + +export const registerMiddleware = async () => { + const ogMw = new OgImageMiddleware({ + App, + Document, + }); + + return [ogMw]; +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.input.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.input.tsx new file mode 100644 index 000000000000..2ef279387fd2 --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.input.tsx @@ -0,0 +1,17 @@ +import type { TagDescriptor } from '@redwoodjs/web' + +import App from './App' +import { Document } from './Document' + +interface Props { + css: string[] + meta?: TagDescriptor[] +} + +export const ServerEntry: React.FC = ({ css, meta }) => { + return ( + + + + ) +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.output.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.output.tsx new file mode 100644 index 000000000000..9fb7d82a95ca --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultTsx.output.tsx @@ -0,0 +1,27 @@ +import OgImageMiddleware from "@redwoodjs/ogimage-gen/middleware"; +import type { TagDescriptor } from '@redwoodjs/web' + +import App from './App' +import { Document } from './Document' + +interface Props { + css: string[] + meta?: TagDescriptor[] +} + +export const ServerEntry: React.FC = ({ css, meta }) => { + return ( + + + + ) +} + +export const registerMiddleware = async () => { + const ogMw = new OgImageMiddleware({ + App, + Document, + }); + + return [ogMw]; +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.input.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.input.tsx new file mode 100644 index 000000000000..1ccc930d5cea --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.input.tsx @@ -0,0 +1,19 @@ +import dns from 'dns' + +import type { UserConfig } from 'vite' +import { defineConfig } from 'vite' + +import redwood from '@redwoodjs/vite' + +// So that Vite will load on localhost instead of `127.0.0.1`. +// See: https://vitejs.dev/config/server-options.html#server-host. +dns.setDefaultResultOrder('verbatim') + +const viteConfig: UserConfig = { + plugins: [redwood()], + optimizeDeps: { + force: true, + }, +} + +export default defineConfig(viteConfig) diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.output.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.output.tsx new file mode 100644 index 000000000000..668d9035598a --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/defaultViteConfig.output.tsx @@ -0,0 +1,20 @@ +import vitePluginOgImageGen from '@redwoodjs/ogimage-gen/plugin' +import dns from 'dns' + +import type { UserConfig } from 'vite' +import { defineConfig } from 'vite' + +import redwood from '@redwoodjs/vite' + +// So that Vite will load on localhost instead of `127.0.0.1`. +// See: https://vitejs.dev/config/server-options.html#server-host. +dns.setDefaultResultOrder('verbatim') + +const viteConfig: UserConfig = { + plugins: [redwood(), vitePluginOgImageGen()], + optimizeDeps: { + force: true, + }, +} + +export default defineConfig(viteConfig) diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.input.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.input.tsx new file mode 100644 index 000000000000..d00756a888fb --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.input.tsx @@ -0,0 +1,27 @@ +import type { TagDescriptor } from '@redwoodjs/web' + +import App from './App' +import { Document } from './Document' + +interface Props { + css: string[] + meta?: TagDescriptor[] +} + +export const ServerEntry: React.FC = ({ css, meta }) => { + return ( + + + + ) +} + +export const registerMiddleware = async () => { + const mojomboMiddleware = () => { + while(true){ + console.log("RedwoodJS is awesome!") + } + } + + return [mojomboMiddleware] +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.output.tsx b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.output.tsx new file mode 100644 index 000000000000..040fd8c37374 --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/__testfixtures__/registerFunctionAlreadyDefined.output.tsx @@ -0,0 +1,33 @@ +import OgImageMiddleware from "@redwoodjs/ogimage-gen/middleware"; +import type { TagDescriptor } from '@redwoodjs/web' + +import App from './App' +import { Document } from './Document' + +interface Props { + css: string[] + meta?: TagDescriptor[] +} + +export const ServerEntry: React.FC = ({ css, meta }) => { + return ( + + + + ) +} + +export const registerMiddleware = async () => { + const mojomboMiddleware = () => { + while(true){ + console.log("RedwoodJS is awesome!") + } + } + + const ogMw = new OgImageMiddleware({ + App, + Document, + }); + + return [mojomboMiddleware, ogMw]; +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/codemodMiddleware.ts b/packages/cli/src/commands/setup/middleware/ogImage/codemodMiddleware.ts new file mode 100644 index 000000000000..5105aee63b77 --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/codemodMiddleware.ts @@ -0,0 +1,129 @@ +import type { FileInfo, API } from 'jscodeshift' + +export default function transform(file: FileInfo, api: API) { + const j = api.jscodeshift + const ast = j(file.source) + + // Insert `import { OgImageMiddleware } from '@redwoodjs/ogimage-gen/middleware'` at the top of the file + const needsImport = + ast.find(j.ImportDeclaration, { + specifiers: [ + { + type: 'ImportDefaultSpecifier', + local: { + name: 'OgImageMiddleware', + }, + }, + ], + source: { + value: '@redwoodjs/ogimage-gen/middleware', + type: 'StringLiteral', + }, + }).length === 0 + if (needsImport) { + ast + .find(j.ImportDeclaration) + .at(0) + .insertBefore( + j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('OgImageMiddleware'))], + j.stringLiteral('@redwoodjs/ogimage-gen/middleware'), + ), + ) + } + + // Find the `registerMiddleware` function + const registerMiddleware = ast.find(j.ExportNamedDeclaration, { + declaration(value) { + if (!value) { + return false + } + + // Handle VariableDeclaration type + if (value.type === 'VariableDeclaration') { + return ( + value.declarations[0].type === 'VariableDeclarator' && + value.declarations[0].id.type === 'Identifier' && + value.declarations[0].id.name === 'registerMiddleware' + ) + } + + // Handle FunctionDeclaration type + if (value.type === 'FunctionDeclaration') { + return ( + value.id?.type === 'Identifier' && + value.id?.name === 'registerMiddleware' + ) + } + + return false + }, + }) + + const appObjectProperty = j.objectProperty( + j.identifier('App'), + j.identifier('App'), + ) + appObjectProperty.shorthand = true + const documentObjectProperty = j.objectProperty( + j.identifier('Document'), + j.identifier('Document'), + ) + documentObjectProperty.shorthand = true + const ogMwDeclaration = j.variableDeclaration('const', [ + j.variableDeclarator( + j.identifier('ogMw'), + j.newExpression(j.identifier('OgImageMiddleware'), [ + j.objectExpression([appObjectProperty, documentObjectProperty]), + ]), + ), + ]) + const arrowFunc = j.arrowFunctionExpression( + [], + j.blockStatement([ + ogMwDeclaration, + j.returnStatement(j.arrayExpression([j.identifier('ogMw')])), + ]), + ) + arrowFunc.async = true + + const needsCompleteRegisterMiddleware = registerMiddleware.length === 0 + if (needsCompleteRegisterMiddleware) { + // If `registerMiddleware` is not defined, define it with the `OgImageMiddleware` included + ast + .find(j.ExportNamedDeclaration) + .at(-1) + .insertAfter( + j.exportNamedDeclaration( + j.variableDeclaration('const', [ + j.variableDeclarator(j.identifier('registerMiddleware'), arrowFunc), + ]), + ), + ) + } else { + // Add `OgImageMiddleware` to the existing `registerMiddleware` function + const returnStatement = registerMiddleware.find(j.ReturnStatement, { + argument: { + type: 'ArrayExpression', + }, + }) + if (returnStatement.length === 0) { + throw new Error( + 'Could not find the return statement in the existing registerMiddleware function', + ) + } + + returnStatement.insertBefore(ogMwDeclaration) + + returnStatement + .find(j.ArrayExpression) + .at(0) + .replaceWith((nodePath) => { + const elements = nodePath.value.elements + elements.push(j.identifier('ogMw')) + return nodePath.value + }) + } + + return ast.toSource() +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/codemodVitePlugin.ts b/packages/cli/src/commands/setup/middleware/ogImage/codemodVitePlugin.ts new file mode 100644 index 000000000000..381547a205e9 --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/codemodVitePlugin.ts @@ -0,0 +1,91 @@ +import type { FileInfo, API } from 'jscodeshift' + +export default function transform(file: FileInfo, api: API) { + const j = api.jscodeshift + const ast = j(file.source) + + // Insert `import vitePluginOgImageGen from '@redwoodjs/ogimage-gen/plugin'` at the top of the file + const needsImport = + ast.find(j.ImportDeclaration, { + specifiers: [ + { + type: 'ImportDefaultSpecifier', + local: { + name: 'vitePluginOgImageGen', + }, + }, + ], + source: { + value: '@redwoodjs/ogimage-gen/plugin', + type: 'StringLiteral', + }, + }).length === 0 + if (needsImport) { + ast + .find(j.ImportDeclaration) + .at(0) + .insertBefore( + j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('vitePluginOgImageGen'))], + j.stringLiteral('@redwoodjs/ogimage-gen/plugin'), + ), + ) + } + + // Find the `viteConfig` variable + const viteConfigVariable = ast.find(j.VariableDeclaration, { + declarations(value) { + if (value.length !== 1) { + return false + } + + const declaration = value[0] + if (declaration.type !== 'VariableDeclarator') { + return false + } + + return ( + declaration.id.type === 'Identifier' && + declaration.id.name === 'viteConfig' + ) + }, + }) + + if (viteConfigVariable.length === 0) { + throw new Error('Could not find the `viteConfig` variable') + } + + // Find the `plugins` array in the `viteConfig` variable + const pluginsArray = viteConfigVariable.find(j.ObjectExpression, { + properties(value) { + if (!value) { + return false + } + + return value.some( + (property) => + property.type === 'ObjectProperty' && + property.key.type === 'Identifier' && + property.key.name === 'plugins', + ) + }, + }) + + if (pluginsArray.length === 0) { + throw new Error( + 'Could not find the `plugins` array in the `viteConfig` variable', + ) + } + + // Add `vitePluginOgImageGen()` to the `plugins` array + pluginsArray + .find(j.ArrayExpression) + .at(0) + .replaceWith((nodePath) => { + const elements = nodePath.value.elements + elements.push(j.callExpression(j.identifier('vitePluginOgImageGen'), [])) + return nodePath.value + }) + + return ast.toSource() +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/ogImage.ts b/packages/cli/src/commands/setup/middleware/ogImage/ogImage.ts new file mode 100644 index 000000000000..687a608a57cc --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/ogImage.ts @@ -0,0 +1,26 @@ +import type { Argv } from 'yargs' + +import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' + +export const command = 'og-image' +export const aliases = ['ogImage', 'ogimage'] +export const description = 'Set up OG Image generation middleware' + +export function builder(yargs: Argv) { + return yargs.option('force', { + alias: 'f', + default: false, + description: 'Overwrite existing configuration', + type: 'boolean', + }) +} + +export async function handler({ force }: { force: boolean }) { + recordTelemetryAttributes({ + command: 'setup middleware og-image', + force, + }) + + const { handler } = await import('./ogImageHandler.js') + return handler({ force }) +} diff --git a/packages/cli/src/commands/setup/middleware/ogImage/ogImageHandler.ts b/packages/cli/src/commands/setup/middleware/ogImage/ogImageHandler.ts new file mode 100644 index 000000000000..130dc3d468fe --- /dev/null +++ b/packages/cli/src/commands/setup/middleware/ogImage/ogImageHandler.ts @@ -0,0 +1,125 @@ +import path from 'node:path' + +import fs from 'fs-extra' +import { Listr } from 'listr2' +import { format } from 'prettier' + +import { addWebPackages, getPrettierOptions } from '@redwoodjs/cli-helpers' +import { getConfig, getPaths } from '@redwoodjs/project-config' + +import { runTransform } from '../../../../lib/runTransform' + +export async function handler({ force }: { force: boolean }) { + const rwPaths = getPaths() + const rootPkgJson = fs.readJSONSync(path.join(rwPaths.base, 'package.json')) + const currentProjectVersion = rootPkgJson.devDependencies['@redwoodjs/core'] + + const notes: string[] = [''] + const tasks = new Listr( + [ + { + title: 'Check prerequisites', + skip: force, + task: () => { + if (!getConfig().experimental?.streamingSsr?.enabled) { + throw new Error( + 'The Streaming SSR experimental feature must be enabled before you can setup middleware.\n\nRun this command to setup streaming ssr: \n yarn rw exp setup-streaming-ssr\n', + ) + } + }, + }, + addWebPackages([`@redwoodjs/ogimage-gen@${currentProjectVersion}`]), + { + title: 'Add OG Image middleware ...', + task: async () => { + const serverEntryPath = rwPaths.web.entryServer + if (serverEntryPath === null) { + throw new Error( + 'Could not find the server entry file. Is your project using the default structure?', + ) + } + + const transformResult = await runTransform({ + transformPath: path.join(__dirname, 'codemodMiddleware.js'), + targetPaths: [serverEntryPath], + }) + + if (transformResult.error) { + throw new Error(transformResult.error) + } + }, + }, + { + title: 'Add OG Image vite plugin ...', + task: async () => { + const viteConfigPath = rwPaths.web.viteConfig + if (viteConfigPath === null) { + throw new Error('Could not find the Vite config file') + } + + const transformResult = await runTransform({ + transformPath: path.join(__dirname, 'codemodVitePlugin.js'), + targetPaths: [viteConfigPath], + }) + + if (transformResult.error) { + throw new Error(transformResult.error) + } + }, + }, + { + title: 'Prettifying changed files', + task: async (_ctx, task) => { + const prettifyPaths = [ + rwPaths.web.entryServer, + rwPaths.web.viteConfig, + ] + for (const prettifyPath of prettifyPaths) { + if (prettifyPath === null) { + throw new Error('Could not find the file to be prettified') + } + try { + const source = fs.readFileSync(prettifyPath, 'utf-8') + const prettierOptions = await getPrettierOptions() + const prettifiedApp = await format(source, { + ...prettierOptions, + parser: 'babel-ts', + }) + + fs.writeFileSync(prettifyPath, prettifiedApp, 'utf-8') + } catch (error) { + task.output = + "Couldn't prettify the changes. Please reformat the files manually if needed." + } + } + }, + }, + { + title: 'One more thing...', + task: () => { + // Note: We avoid logging in the task because it can mess up the formatting of the text and we are often looking to maintain some basic indentation and such. + notes.push( + "og:image generation is almost ready to go! You'll need to add playwright as a dependency to the api side and then install the headless browser packages:", + ) + notes.push('') + notes.push(' yarn workspace api add playwright') + notes.push(' cd api') + notes.push(' yarn playwright install') + notes.push('') + notes.push( + 'Depending on how your host is configured you may need to install additional dependencies first. If so, the `playwright install` step will error out and give you the command to run to install those deps.', + ) + }, + }, + ], + { rendererOptions: { collapseSubtasks: false } }, + ) + + try { + await tasks.run() + console.log(notes.join('\n')) + } catch (e: any) { + console.error(e.message) + process.exit(e?.exitCode || 1) + } +} diff --git a/packages/cli/src/commands/setup/graphql/features/fragments/runTransform.ts b/packages/cli/src/lib/runTransform.ts similarity index 100% rename from packages/cli/src/commands/setup/graphql/features/fragments/runTransform.ts rename to packages/cli/src/lib/runTransform.ts diff --git a/packages/cli/src/testUtils/matchFolderTransform.ts b/packages/cli/src/testUtils/matchFolderTransform.ts index 164873756ddf..4cdcc50fa135 100644 --- a/packages/cli/src/testUtils/matchFolderTransform.ts +++ b/packages/cli/src/testUtils/matchFolderTransform.ts @@ -1,5 +1,5 @@ import { createRequire } from 'node:module' -import path from 'path' +import path from 'node:path' import fg from 'fast-glob' import fse from 'fs-extra' diff --git a/packages/cli/src/testUtils/matchInlineTransformSnapshot.ts b/packages/cli/src/testUtils/matchInlineTransformSnapshot.ts index b28ed62d8fe4..d1ef031710ec 100644 --- a/packages/cli/src/testUtils/matchInlineTransformSnapshot.ts +++ b/packages/cli/src/testUtils/matchInlineTransformSnapshot.ts @@ -1,6 +1,6 @@ -import fs from 'fs' +import fs from 'node:fs' import { createRequire } from 'node:module' -import path from 'path' +import path from 'node:path' import tempy from 'tempy' import { expect } from 'vitest' diff --git a/packages/cli/src/testUtils/matchTransformSnapshot.ts b/packages/cli/src/testUtils/matchTransformSnapshot.ts new file mode 100644 index 000000000000..3f3ee546ed5a --- /dev/null +++ b/packages/cli/src/testUtils/matchTransformSnapshot.ts @@ -0,0 +1,87 @@ +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +import tempy from 'tempy' +import { expect } from 'vitest' + +import runTransform from '../testLib/runTransform' + +import { formatCode } from './index' + +const require = createRequire(import.meta.url) + +export interface MatchTransformSnapshotFunction { + ( + transformName: string, + fixtureName?: string, + parser?: 'ts' | 'tsx', + ): Promise +} + +export const matchTransformSnapshot: MatchTransformSnapshotFunction = async ( + transformName, + fixtureName, + parser, +) => { + const tempFilePath = tempy.file() + + // Looks up the path of the caller + const testPath = expect.getState().testPath + + if (!testPath) { + throw new Error('Could not find test path') + } + + let fixturePath + + const maybeFixturePath = path.join( + testPath, + '../../__testfixtures__', + `${fixtureName}.input`, + ) + + for (const extension of ['ts', 'tsx', 'js', 'jsx']) { + try { + fixturePath = require.resolve(`${maybeFixturePath}.${extension}`) + } catch (e) { + continue + } + } + + if (!fixturePath) { + throw new Error( + `Could not find fixture for ${fixtureName} in ${maybeFixturePath}`, + ) + } + + const transformPath = require.resolve( + path.join(testPath, '../../', `${transformName}.ts`), + ) + + // Step 1: Copy fixture to temp file + fs.copyFileSync(fixturePath, tempFilePath, fs.constants.COPYFILE_FICLONE) + + // Step 2: Run transform against temp file + await runTransform({ + transformPath, + targetPaths: [tempFilePath], + parser, + options: { + verbose: 1, + print: true, + }, + }) + + // Step 3: Read modified file and snapshot + const transformedContent = fs.readFileSync(tempFilePath, 'utf-8') + + const expectedOutput = fs.readFileSync( + fixturePath.replace('.input.', '.output.'), + 'utf-8', + ) + + expect(await formatCode(transformedContent)).toEqual( + await formatCode(expectedOutput), + ) +} diff --git a/packages/cli/testUtils.d.ts b/packages/cli/testUtils.d.ts index 55e108211d1f..08a9ec8b30d7 100644 --- a/packages/cli/testUtils.d.ts +++ b/packages/cli/testUtils.d.ts @@ -34,6 +34,12 @@ declare module 'jscodeshift/dist/testUtils' { } // @NOTE: Redefining types, because they get lost when importing from the testUtils file +type MatchTransformSnapshotFunction = ( + transformName: string, + fixtureName?: string, + parser?: 'ts' | 'tsx', +) => Promise + type MatchFolderTransformFunction = ( transformFunctionOrName: (() => any) | string, fixtureName: string, @@ -57,6 +63,7 @@ type MatchInlineTransformSnapshotFunction = ( // These files gets loaded in vitest setup, so becomes available globally in tests declare global { + var matchTransformSnapshot: MatchTransformSnapshotFunction var matchInlineTransformSnapshot: MatchInlineTransformSnapshotFunction var matchFolderTransform: MatchFolderTransformFunction diff --git a/packages/cli/vitest.codemods.setup.ts b/packages/cli/vitest.codemods.setup.ts index 59959ac03a4b..02cc62ccc64e 100644 --- a/packages/cli/vitest.codemods.setup.ts +++ b/packages/cli/vitest.codemods.setup.ts @@ -13,6 +13,9 @@ import { formatCode } from './src/testUtils' globalThis.matchInlineTransformSnapshot = ( await import('./src/testUtils/matchInlineTransformSnapshot') ).matchInlineTransformSnapshot +globalThis.matchTransformSnapshot = ( + await import('./src/testUtils/matchTransformSnapshot') +).matchTransformSnapshot globalThis.matchFolderTransform = ( await import('./src/testUtils/matchFolderTransform') ).matchFolderTransform diff --git a/packages/codemods/src/testUtils/matchTransformSnapshot.ts b/packages/codemods/src/testUtils/matchTransformSnapshot.ts index 96e4ceb4f81f..e984ece49a50 100644 --- a/packages/codemods/src/testUtils/matchTransformSnapshot.ts +++ b/packages/codemods/src/testUtils/matchTransformSnapshot.ts @@ -9,7 +9,11 @@ import runTransform from '../lib/runTransform' import { formatCode } from './index' export interface MatchTransformSnapshotFunction { - (transformName: string, fixtureName?: string, parser?: 'ts' | 'tsx'): void + ( + transformName: string, + fixtureName?: string, + parser?: 'ts' | 'tsx', + ): Promise } export const matchTransformSnapshot: MatchTransformSnapshotFunction = async (