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 (