Skip to content

Commit

Permalink
fix(esm): get lint working (#10152)
Browse files Browse the repository at this point in the history
Started this PR so that an ESM project could run `yarn rw lint` (right
now you'd 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.
```

) but didn't quite fix that yet—that seems to require Prettier v3 which
may be possible to release in a minor thanks to @Josh-Walker-GM's
efforts porting us over to Vitest!

While this PR doesn't get `yarn rw lint` working, it does get a bunch of
generators and setup commands working that use Prettier since they were
using `require` to import the prettier config file.

---------

Co-authored-by: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com>
  • Loading branch information
jtoar and Josh-Walker-GM committed Mar 9, 2024
1 parent 33b9021 commit 2f0f5c9
Show file tree
Hide file tree
Showing 42 changed files with 422 additions and 362 deletions.
13 changes: 13 additions & 0 deletions .changesets/10152.md
Original file line number Diff line number Diff line change
@@ -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.
```
12 changes: 6 additions & 6 deletions packages/cli-helpers/src/auth/__tests__/authFiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand All @@ -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,
})
Expand All @@ -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,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/cli-helpers/src/auth/__tests__/authTasks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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')
Expand Down
121 changes: 58 additions & 63 deletions packages/cli-helpers/src/auth/authFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>>(
(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<string, string> = {}

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
}
Expand Down
6 changes: 3 additions & 3 deletions packages/cli-helpers/src/auth/authTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
},
Expand Down Expand Up @@ -448,7 +448,7 @@ export const generateAuthApiFiles = <Renderer extends typeof ListrRenderer>(

// 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'
Expand Down
13 changes: 10 additions & 3 deletions packages/cli-helpers/src/lib/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from 'path'

import { vi, test, expect } from 'vitest'

import { prettify } from '../index.js'
Expand All @@ -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 }
Expand All @@ -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()
})
14 changes: 9 additions & 5 deletions packages/cli-helpers/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -70,10 +72,10 @@ export const prettierOptions = () => {
}
}

export const prettify = (
export const prettify = async (
templateFilename: string,
renderedTemplate: string
): string => {
): Promise<string> => {
// We format .js and .css templates, we need to tell prettier which parser
// we're using.
// https://prettier.io/docs/en/options.html#parser
Expand All @@ -88,8 +90,10 @@ export const prettify = (
return renderedTemplate
}

const prettierOptions = await getPrettierOptions()

return format(renderedTemplate, {
...prettierOptions(),
...prettierOptions,
parser,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {})
})
Expand All @@ -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',
Expand All @@ -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))
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/commands/destroy/page/__tests__/page.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]: [
'<Routes>',
' <Route path="/about" page={AboutPage} name="about" />',
Expand Down Expand Up @@ -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]: [
'<Routes>',
' <Route path="/about" page={AboutPage} name="about" />',
Expand Down
Loading

0 comments on commit 2f0f5c9

Please sign in to comment.