diff --git a/.changesets/10152.md b/.changesets/10152.md new file mode 100644 index 000000000000..eea6adfc65cd --- /dev/null +++ b/.changesets/10152.md @@ -0,0 +1,13 @@ +- fix(esm): get lint working (#10152) by @jtoar + +This PR fixes `yarn rw lint` and some generators and setup commands for ESM projects. +Before projects would get the following error: + +``` +Oops! Something went wrong! :( + +ESLint: 8.55.0 + +Error [ERR_REQUIRE_ESM]: require() of ES Module ~/redwood-app/prettier.config.js from ~/redwood-app/node_modules/prettier/third-party.js not supported. +Instead change the require of prettier.config.js in ~/redwood-app/node_modules/prettier/third-party.js to a dynamic import() which is available in all CommonJS modules. +``` diff --git a/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts b/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts index 01a19546d988..6107f663a0c3 100644 --- a/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts +++ b/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts @@ -39,9 +39,9 @@ beforeEach(() => { vi.mocked(isTypeScriptProject).mockReturnValue(true) }) -it('generates a record of TS files', () => { +it('generates a record of TS files', async () => { const filePaths = Object.keys( - apiSideFiles({ + await apiSideFiles({ basedir: path.join(__dirname, 'fixtures/supertokensSetup'), webAuthn: false, }) @@ -54,11 +54,11 @@ it('generates a record of TS files', () => { ]) }) -it('generates a record of JS files', () => { +it('generates a record of JS files', async () => { vi.mocked(isTypeScriptProject).mockReturnValue(false) const filePaths = Object.keys( - apiSideFiles({ + await apiSideFiles({ basedir: path.join(__dirname, 'fixtures/supertokensSetup'), webAuthn: false, }) @@ -71,8 +71,8 @@ it('generates a record of JS files', () => { ]) }) -it('generates a record of webAuthn files', () => { - const filesRecord = apiSideFiles({ +it('generates a record of webAuthn files', async () => { + const filesRecord = await apiSideFiles({ basedir: path.join(__dirname, 'fixtures/dbAuthSetup'), webAuthn: true, }) diff --git a/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts b/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts index 3427788359af..1b4a04d9c404 100644 --- a/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts +++ b/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts @@ -644,7 +644,7 @@ describe('authTasks', () => { ).toMatchSnapshot() }) - it('writes an auth.js file for JS projects', () => { + it('writes an auth.js file for JS projects', async () => { vi.mocked(isTypeScriptProject).mockReturnValue(false) vol.fromJSON({ @@ -656,7 +656,7 @@ describe('authTasks', () => { provider: 'auth0', setupMode: 'FORCE', } - createWebAuth(getPaths().base, false).task(ctx) + await createWebAuth(getPaths().base, false).task(ctx) expect( fs.readFileSync(path.join(getPaths().web.src, 'auth.js'), 'utf-8') diff --git a/packages/cli-helpers/src/auth/authFiles.ts b/packages/cli-helpers/src/auth/authFiles.ts index c64f48f8141a..28ce77d1ef53 100644 --- a/packages/cli-helpers/src/auth/authFiles.ts +++ b/packages/cli-helpers/src/auth/authFiles.ts @@ -24,76 +24,71 @@ interface FilesArgs { * } * ``` */ -export const apiSideFiles = ({ basedir, webAuthn }: FilesArgs) => { +export const apiSideFiles = async ({ basedir, webAuthn }: FilesArgs) => { const apiSrcPath = getPaths().api.src const apiBaseTemplatePath = path.join(basedir, 'templates', 'api') const templateDirectories = fs.readdirSync(apiBaseTemplatePath) - const filesRecord = templateDirectories.reduce>( - (acc, dir) => { - const templateFiles = fs.readdirSync(path.join(apiBaseTemplatePath, dir)) - const filePaths = templateFiles - .filter((fileName) => { - const fileNameParts = fileName.split('.') - // Remove all webAuthn files. We'll handle those in the next step - return ( - fileNameParts.length <= 3 || fileNameParts.at(-3) !== 'webAuthn' - ) - }) - .map((fileName) => { - // remove "template" from the end, and change from {ts,tsx} to {js,jsx} for - // JavaScript projects - let outputFileName = fileName.replace(/\.template$/, '') - if (!isTypeScriptProject()) { - outputFileName = outputFileName.replace(/\.ts(x?)$/, '.js$1') - } - - if (!webAuthn) { - return { templateFileName: fileName, outputFileName } - } - - // Insert "webAuthn." before the second to last part - const webAuthnFileName = fileName - .split('.') - .reverse() - .map((part, i) => (i === 1 ? 'webAuthn.' + part : part)) - .reverse() - .join('.') - - // Favor the abc.xyz.webAuthn.ts.template file if it exists, otherwise - // just go with the "normal" filename - if (templateFiles.includes(webAuthnFileName)) { - return { templateFileName: webAuthnFileName, outputFileName } - } else { - return { templateFileName: fileName, outputFileName } - } - }) - .map((f) => { - const templateFilePath = path.join( - apiBaseTemplatePath, - dir, - f.templateFileName - ) - const outputFilePath = path.join(apiSrcPath, dir, f.outputFileName) - - return { templateFilePath, outputFilePath } - }) - - filePaths.forEach((paths) => { - const content = fs.readFileSync(paths.templateFilePath, 'utf8') - - acc = { - ...acc, - [paths.outputFilePath]: isTypeScriptProject() - ? content - : transformTSToJS(paths.outputFilePath, content), + let filesRecord: Record = {} + + for (const dir of templateDirectories) { + const templateFiles = fs.readdirSync(path.join(apiBaseTemplatePath, dir)) + const filePaths = templateFiles + .filter((fileName) => { + const fileNameParts = fileName.split('.') + // Remove all webAuthn files. We'll handle those in the next step + return fileNameParts.length <= 3 || fileNameParts.at(-3) !== 'webAuthn' + }) + .map((fileName) => { + // remove "template" from the end, and change from {ts,tsx} to {js,jsx} for + // JavaScript projects + let outputFileName = fileName.replace(/\.template$/, '') + if (!isTypeScriptProject()) { + outputFileName = outputFileName.replace(/\.ts(x?)$/, '.js$1') + } + + if (!webAuthn) { + return { templateFileName: fileName, outputFileName } + } + + // Insert "webAuthn." before the second to last part + const webAuthnFileName = fileName + .split('.') + .reverse() + .map((part, i) => (i === 1 ? 'webAuthn.' + part : part)) + .reverse() + .join('.') + + // Favor the abc.xyz.webAuthn.ts.template file if it exists, otherwise + // just go with the "normal" filename + if (templateFiles.includes(webAuthnFileName)) { + return { templateFileName: webAuthnFileName, outputFileName } + } else { + return { templateFileName: fileName, outputFileName } } }) + .map((f) => { + const templateFilePath = path.join( + apiBaseTemplatePath, + dir, + f.templateFileName + ) + const outputFilePath = path.join(apiSrcPath, dir, f.outputFileName) + + return { templateFilePath, outputFilePath } + }) + + for (const paths of filePaths) { + const content = fs.readFileSync(paths.templateFilePath, 'utf8') - return acc - }, - {} - ) + filesRecord = { + ...filesRecord, + [paths.outputFilePath]: isTypeScriptProject() + ? content + : await transformTSToJS(paths.outputFilePath, content), + } + } + } return filesRecord } diff --git a/packages/cli-helpers/src/auth/authTasks.ts b/packages/cli-helpers/src/auth/authTasks.ts index c4d515c7e6d2..8c90351d4a93 100644 --- a/packages/cli-helpers/src/auth/authTasks.ts +++ b/packages/cli-helpers/src/auth/authTasks.ts @@ -362,7 +362,7 @@ export const createWebAuth = (basedir: string, webAuthn: boolean) => { return { title: `Creating web/src/auth.${ext}`, - task: (ctx: AuthGeneratorCtx) => { + task: async (ctx: AuthGeneratorCtx) => { // @MARK - finding unused file name here, // We should only use an unused filename, if the user is CHOOSING not to replace the existing provider @@ -399,7 +399,7 @@ export const createWebAuth = (basedir: string, webAuthn: boolean) => { template = isTSProject ? template - : transformTSToJS(authFileName, template) + : await transformTSToJS(authFileName, template) fs.writeFileSync(authFileName, template) }, @@ -448,7 +448,7 @@ export const generateAuthApiFiles = ( // The keys in `filesRecord` are the full paths to where the file contents, // which is the values in `filesRecord`, will be written. - let filesRecord = apiSideFiles({ basedir, webAuthn }) + let filesRecord = await apiSideFiles({ basedir, webAuthn }) // Always overwrite files in force mode, no need to prompt let existingFiles: ExistingFiles = 'FAIL' diff --git a/packages/cli-helpers/src/lib/__tests__/index.test.ts b/packages/cli-helpers/src/lib/__tests__/index.test.ts index e2592f4fac2d..e3974872a26f 100644 --- a/packages/cli-helpers/src/lib/__tests__/index.test.ts +++ b/packages/cli-helpers/src/lib/__tests__/index.test.ts @@ -1,3 +1,5 @@ +import path from 'path' + import { vi, test, expect } from 'vitest' import { prettify } from '../index.js' @@ -6,13 +8,16 @@ vi.mock('../paths', () => { return { getPaths: () => { return { - base: '../../../../__fixtures__/example-todo-main', + base: path.resolve( + __dirname, + '../../../../../__fixtures__/example-todo-main' + ), } }, } }) -test('prettify formats tsx content', () => { +test('prettify formats tsx content', async () => { const content = `import React from 'react' interface Props { foo: number, bar: number } @@ -30,5 +35,7 @@ test('prettify formats tsx content', () => { return <>{foo}, {bar}}` - expect(prettify('FooBarComponent.template.tsx', content)).toMatchSnapshot() + expect( + await prettify('FooBarComponent.template.tsx', content) + ).toMatchSnapshot() }) diff --git a/packages/cli-helpers/src/lib/index.ts b/packages/cli-helpers/src/lib/index.ts index 1bc77ed61bea..a9676cd33a1a 100644 --- a/packages/cli-helpers/src/lib/index.ts +++ b/packages/cli-helpers/src/lib/index.ts @@ -52,9 +52,11 @@ export const transformTSToJS = (filename: string, content: string) => { /** * This returns the config present in `prettier.config.js` of a Redwood project. */ -export const prettierOptions = () => { +export const getPrettierOptions = async () => { try { - const options = require(path.join(getPaths().base, 'prettier.config.js')) + const { default: options } = await import( + `file://${path.join(getPaths().base, 'prettier.config.js')}` + ) if (options.tailwindConfig?.startsWith('.')) { // Make this work with --cwd @@ -70,10 +72,10 @@ export const prettierOptions = () => { } } -export const prettify = ( +export const prettify = async ( templateFilename: string, renderedTemplate: string -): string => { +): Promise => { // We format .js and .css templates, we need to tell prettier which parser // we're using. // https://prettier.io/docs/en/options.html#parser @@ -88,8 +90,10 @@ export const prettify = ( return renderedTemplate } + const prettierOptions = await getPrettierOptions() + return format(renderedTemplate, { - ...prettierOptions(), + ...prettierOptions, parser, }) } diff --git a/packages/cli/src/commands/destroy/component/__tests__/component.test.js b/packages/cli/src/commands/destroy/component/__tests__/component.test.js index 93ad25f7b2a0..d236a6622c4f 100644 --- a/packages/cli/src/commands/destroy/component/__tests__/component.test.js +++ b/packages/cli/src/commands/destroy/component/__tests__/component.test.js @@ -17,8 +17,8 @@ import '../../../../lib/test' import { files } from '../../../generate/component/component' import { tasks } from '../component' -beforeEach(() => { - vol.fromJSON(files({ name: 'About' })) +beforeEach(async () => { + vol.fromJSON(await files({ name: 'About' })) vi.spyOn(console, 'info').mockImplementation(() => {}) vi.spyOn(console, 'log').mockImplementation(() => {}) }) @@ -35,15 +35,15 @@ test('destroys component files', async () => { const t = tasks({ componentName: 'component', filesFn: files, name: 'About' }) t.options.renderer = 'silent' - return t.run().then(() => { - const generatedFiles = Object.keys(files({ name: 'About' })) + return t.run().then(async () => { + const generatedFiles = Object.keys(await files({ name: 'About' })) expect(generatedFiles.length).toEqual(unlinkSpy.mock.calls.length) generatedFiles.forEach((f) => expect(unlinkSpy).toHaveBeenCalledWith(f)) }) }) test('destroys component files including stories and tests', async () => { - vol.fromJSON(files({ name: 'About', stories: true, tests: true })) + vol.fromJSON(await files({ name: 'About', stories: true, tests: true })) const unlinkSpy = vi.spyOn(fs, 'unlinkSync') const t = tasks({ componentName: 'component', @@ -54,9 +54,9 @@ test('destroys component files including stories and tests', async () => { }) t.options.renderer = 'silent' - return t.run().then(() => { + return t.run().then(async () => { const generatedFiles = Object.keys( - files({ name: 'About', stories: true, tests: true }) + await files({ name: 'About', stories: true, tests: true }) ) expect(generatedFiles.length).toEqual(unlinkSpy.mock.calls.length) generatedFiles.forEach((f) => expect(unlinkSpy).toHaveBeenCalledWith(f)) diff --git a/packages/cli/src/commands/destroy/page/__tests__/page.test.js b/packages/cli/src/commands/destroy/page/__tests__/page.test.js index e219734d4d07..6c32c7d0b0f2 100644 --- a/packages/cli/src/commands/destroy/page/__tests__/page.test.js +++ b/packages/cli/src/commands/destroy/page/__tests__/page.test.js @@ -18,9 +18,10 @@ import { getPaths } from '../../../../lib' import { files } from '../../../generate/page/page' import { tasks } from '../page' -beforeEach(() => { +beforeEach(async () => { + const f = await files({ name: 'About' }) vol.fromJSON({ - ...files({ name: 'About' }), + ...f, [getPaths().web.routes]: [ '', ' ', @@ -50,8 +51,9 @@ test('destroys page files', async () => { test('destroys page files with stories and tests', async () => { const fileOptions = { name: 'About', stories: true, tests: true } + const f = await files(fileOptions) vol.fromJSON({ - ...files(fileOptions), + ...f, [getPaths().web.routes]: [ '', ' ', diff --git a/packages/cli/src/commands/experimental/setupRscHandler.js b/packages/cli/src/commands/experimental/setupRscHandler.js index b0ae3a54959c..2dbddc3eb0ce 100644 --- a/packages/cli/src/commands/experimental/setupRscHandler.js +++ b/packages/cli/src/commands/experimental/setupRscHandler.js @@ -303,7 +303,7 @@ export const handler = async ({ force, verbose }) => { writeFile( tsconfigPath, - prettify('tsconfig.json', JSON.stringify(tsconfig, null, 2)), + await prettify('tsconfig.json', JSON.stringify(tsconfig, null, 2)), { overwriteExisting: true, } diff --git a/packages/cli/src/commands/generate/__tests__/helpers.test.js b/packages/cli/src/commands/generate/__tests__/helpers.test.js index 5383ea2156fc..f9a86508de90 100644 --- a/packages/cli/src/commands/generate/__tests__/helpers.test.js +++ b/packages/cli/src/commands/generate/__tests__/helpers.test.js @@ -79,11 +79,11 @@ test('customOrDefaultTemplatePath returns the app path with proper side, generat ) }) -test('templateForComponentFile creates a proper output path for files', () => { +test('templateForComponentFile creates a proper output path for files', async () => { const names = ['FooBar', 'fooBar', 'foo-bar', 'foo_bar'] - names.forEach((name) => { - const output = helpers.templateForComponentFile({ + for (const name of names) { + const output = await helpers.templateForComponentFile({ name: name, suffix: 'Page', webPathSection: 'pages', @@ -95,14 +95,14 @@ test('templateForComponentFile creates a proper output path for files', () => { expect(output[0]).toEqual( path.normalize('/path/to/project/web/src/pages/FooBarPage/FooBarPage.js') ) - }) + } }) -test('templateForComponentFile creates a proper output path for files with all caps in component name', () => { +test('templateForComponentFile creates a proper output path for files with all caps in component name', async () => { const names = ['FOO_BAR', 'FOO-BAR', 'FOOBAR'] - names.forEach((name) => { - const output = helpers.templateForComponentFile({ + for (const name of names) { + const output = await helpers.templateForComponentFile({ name: name, suffix: 'Page', webPathSection: 'pages', @@ -114,14 +114,14 @@ test('templateForComponentFile creates a proper output path for files with all c expect(output[0]).toEqual( path.normalize('/path/to/project/web/src/pages/FOOBARPage/FOOBARPage.js') ) - }) + } }) -test('templateForComponentFile creates a proper output path for files for starting with uppercase and ending with lowercase', () => { +test('templateForComponentFile creates a proper output path for files for starting with uppercase and ending with lowercase', async () => { const names = ['FOOBar', 'FOO-Bar', 'FOO_Bar'] - names.forEach((name) => { - const output = helpers.templateForComponentFile({ + for (const name of names) { + const output = await helpers.templateForComponentFile({ name: name, suffix: 'Page', webPathSection: 'pages', @@ -133,14 +133,14 @@ test('templateForComponentFile creates a proper output path for files for starti expect(output[0]).toEqual( path.normalize('/path/to/project/web/src/pages/FOOBarPage/FOOBarPage.js') ) - }) + } }) -test('templateForComponentFile creates a proper output path for files with uppercase after special characters in component name', () => { +test('templateForComponentFile creates a proper output path for files with uppercase after special characters in component name', async () => { const names = ['ABtest', 'aBtest', 'a-Btest', 'a_Btest'] - names.forEach((name) => { - const output = helpers.templateForComponentFile({ + for (const name of names) { + const output = await helpers.templateForComponentFile({ name: name, suffix: 'Page', webPathSection: 'pages', @@ -152,11 +152,11 @@ test('templateForComponentFile creates a proper output path for files with upper expect(output[0]).toEqual( path.normalize('/path/to/project/web/src/pages/ABtestPage/ABtestPage.js') ) - }) + } }) -test('templateForComponentFile can create a path in /web', () => { - const output = helpers.templateForComponentFile({ +test('templateForComponentFile can create a path in /web', async () => { + const output = await helpers.templateForComponentFile({ name: 'Home', suffix: 'Page', webPathSection: 'pages', @@ -170,8 +170,8 @@ test('templateForComponentFile can create a path in /web', () => { ) }) -test('templateForComponentFile can create a path in /api', () => { - const output = helpers.templateForComponentFile({ +test('templateForComponentFile can create a path in /api', async () => { + const output = await helpers.templateForComponentFile({ name: 'Home', suffix: 'Page', apiPathSection: 'services', @@ -185,8 +185,8 @@ test('templateForComponentFile can create a path in /api', () => { ) }) -test('templateForComponentFile can override generated component name', () => { - const output = helpers.templateForComponentFile({ +test('templateForComponentFile can override generated component name', async () => { + const output = await helpers.templateForComponentFile({ name: 'Home', componentName: 'Hobbiton', webPathSection: 'pages', @@ -200,8 +200,8 @@ test('templateForComponentFile can override generated component name', () => { ) }) -test('templateForComponentFile can override file extension', () => { - const output = helpers.templateForComponentFile({ +test('templateForComponentFile can override file extension', async () => { + const output = await helpers.templateForComponentFile({ name: 'Home', suffix: 'Page', extension: '.txt', @@ -216,8 +216,8 @@ test('templateForComponentFile can override file extension', () => { ) }) -test('templateForComponentFile can override output path', () => { - const output = helpers.templateForComponentFile({ +test('templateForComponentFile can override output path', async () => { + const output = await helpers.templateForComponentFile({ name: 'func', apiPathSection: 'functions', generator: 'function', @@ -231,8 +231,8 @@ test('templateForComponentFile can override output path', () => { ) }) -test('templateForComponentFile creates a template', () => { - const output = helpers.templateForComponentFile({ +test('templateForComponentFile creates a template', async () => { + const output = await helpers.templateForComponentFile({ name: 'FooBar', suffix: 'Page', webPathSection: 'pages', diff --git a/packages/cli/src/commands/generate/cell/cell.js b/packages/cli/src/commands/generate/cell/cell.js index e54fb95fd737..4adc7f6c1608 100644 --- a/packages/cli/src/commands/generate/cell/cell.js +++ b/packages/cli/src/commands/generate/cell/cell.js @@ -78,7 +78,7 @@ export const files = async ({ name, typescript, ...options }) => { } const extension = typescript ? '.tsx' : '.jsx' - const cellFile = templateForComponentFile({ + const cellFile = await templateForComponentFile({ name: cellName, suffix: COMPONENT_SUFFIX, extension, @@ -92,7 +92,7 @@ export const files = async ({ name, typescript, ...options }) => { }, }) - const testFile = templateForComponentFile({ + const testFile = await templateForComponentFile({ name: cellName, suffix: COMPONENT_SUFFIX, extension: `.test${extension}`, @@ -101,7 +101,7 @@ export const files = async ({ name, typescript, ...options }) => { templatePath: 'test.js.template', }) - const storiesFile = templateForComponentFile({ + const storiesFile = await templateForComponentFile({ name: cellName, suffix: COMPONENT_SUFFIX, extension: `.stories${extension}`, @@ -110,7 +110,7 @@ export const files = async ({ name, typescript, ...options }) => { templatePath: 'stories.tsx.template', }) - const mockFile = templateForComponentFile({ + const mockFile = await templateForComponentFile({ name: cellName, suffix: COMPONENT_SUFFIX, extension: typescript ? '.mock.ts' : '.mock.js', @@ -142,14 +142,18 @@ export const files = async ({ name, typescript, ...options }) => { // "path/to/fileA": "<<