Skip to content

Commit

Permalink
fix(esm): fix initial esm blockers for redwood apps (#10083)
Browse files Browse the repository at this point in the history
This PR makes some initial fixes that were required for making a Redwood
app ESM. @Josh-Walker-GM and I tested by making a new project,
specifying `"type": "module"` in all the package.jsons, and trying to
get things to work (namely, `yarn rw build` and `yarn rw serve`). We'll
ramp up our testing to one of the test project fixtures after we get
these initial things fixed. Everything here is backwards-compatible.

Some of the changes we had to make were in the project itself. Namely,
specifying file extensions for relative imports (`import { db } from
'src/lib/db.js'` instead of `import { db } from 'src/lib/db'`). But
others needed to be made at the framework level:

- esbuild has to emit ESM

- `packages/api-server/src/plugins/lambdaLoader.ts` can't use `require`
to import an ES module

- Babel plugins have to emit relative file imports with extensions

- The `@redwoodjs/graphql-server` package needs to be built differently
to avoid having to do this...

  ```ts
  // ./api/src/functions/graphql.ts
  import pkg from '@redwoodjs/graphql-server'
  const { createGraphQLHandler } = pkg
  ```

Not 100% sure why, but Babel makes some exports getters instead of just
exporting them statically. Better to have less magic going on in dist.

Some manual changes to a project are inevitable (the file extensions),
but we'd like to minimize them as much as possible. Here's a few that
were necessary that we haven't figured out yet...

- dist imports, like `import ... from '@redwoodjs/api/logger'`, don't
work

They need to be changed to `import ... from
'@redwoodjs/api/logger/index.js'`. It feels like the only way to really
fix this one is to use `exports` in the package.json, but if I remember
correctly, that requires tsconfig settings that requires ESM

- `redwood` is not a function in `./web/vite.config.ts`:

  We haven't gotten to the bottom of this one yet:

  ```
failed to load config from ~/redwood/redwood-app-esm/web/vite.config.mts

file:///~/redwood/redwood-app-esm/web/vite.config.mts.timestamp-1709251612918-
  6f6a7f8efad05.mjs:7
    plugins: [redwood()]
              ^

  TypeError: redwood is not a function
at
file:///~/redwood/redwood-app-esm/web/vite.config.mts.timestamp-1709251612918-
  6f6a7f8efad05.mjs:7:13
  ```

  The fix is accessing `redwood.default`:

<img width="685" alt="image"
src="https://github.com/redwoodjs/redwood/assets/32992335/4f09fea4-42d3-49a4-9339-74b42c90621e">
  • Loading branch information
jtoar authored and ahaywood committed Mar 1, 2024
1 parent 5c21012 commit 1bc3ecd
Show file tree
Hide file tree
Showing 22 changed files with 201 additions and 85 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Unreleased

- fix(esm): fix initial ESM blockers for Redwood apps (#10083) by @jtoar and @Josh-Walker-GM

This PR makes some initial fixes that were required for making a Redwood app ESM. Redwood apps aren't ready to transition to ESM yet, but we're working towards it and these changes were backwards-compatible.
If you're interested in trying out ESM, there will be an experimental setup command in the future. For now you'd have to make manual changes to your project:

- dist imports, like `import ... from '@redwoodjs/api/logger'` need to be changed to `import ... from '@redwoodjs/api/logger/index.js'`
- The Redwood Vite plugin in `web/vite.config.ts` needs to be changed to `redwood.default` before being invoked

There are probably many others still depending on your project. Again, we don't recommend actually doing this yet, but are enumerating things just to be transparent about the changes in this release.

- fix(deploy): handle server file (#10061)

This fixes the CLI commands for Coherence and Flightcontrol. For Coherence, it fixes a bug introduced in the last patch where the logic for detecting the server file in the setup command (`yarn rw setup deploy coherence`) was flipped. For Flightcontrol, it updates the setup command (`yarn rw setup deploy flightcontrol`) so that it handles Corepack and updates the corresponding deploy command (`yarn rw deploy flightcontrol`) so that it detects the server file similar to the Coherence fix.
Expand Down

This file was deleted.

49 changes: 22 additions & 27 deletions packages/api-server/src/plugins/lambdaLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,32 @@ export const setLambdaFunctions = async (foundFunctions: string[]) => {
const tsImport = Date.now()
console.log(chalk.dim.italic('Importing Server Functions... '))

const imports = foundFunctions.map((fnPath) => {
return new Promise((resolve) => {
const ts = Date.now()
const routeName = path.basename(fnPath).replace('.js', '')

const { handler } = require(fnPath)
LAMBDA_FUNCTIONS[routeName] = handler
if (!handler) {
console.warn(
routeName,
'at',
fnPath,
'does not have a function called handler defined.'
)
}
// TODO: Use terminal link.
console.log(
chalk.magenta('/' + routeName),
chalk.dim.italic(Date.now() - ts + ' ms')
const imports = foundFunctions.map(async (fnPath) => {
const ts = Date.now()
const routeName = path.basename(fnPath).replace('.js', '')

const { handler } = await import(`file://${fnPath}`)
LAMBDA_FUNCTIONS[routeName] = handler
if (!handler) {
console.warn(
routeName,
'at',
fnPath,
'does not have a function called handler defined.'
)
return resolve(true)
})
})

Promise.all(imports).then((_results) => {
}
// TODO: Use terminal link.
console.log(
chalk.dim.italic(
'...Done importing in ' + (Date.now() - tsImport) + ' ms'
)
chalk.magenta('/' + routeName),
chalk.dim.italic(Date.now() - ts + ' ms')
)
})

await Promise.all(imports)

console.log(
chalk.dim.italic('...Done importing in ' + (Date.now() - tsImport) + ' ms')
)
}

type LoadFunctionsFromDistOptions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
38 changes: 24 additions & 14 deletions packages/babel-config/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from 'path'
import type { PluginOptions, PluginTarget, TransformOptions } from '@babel/core'
import { transformAsync } from '@babel/core'

import { getPaths } from '@redwoodjs/project-config'
import { getPaths, projectSideIsEsm } from '@redwoodjs/project-config'

import type { RegisterHookOptions } from './common'
import {
Expand Down Expand Up @@ -74,11 +74,10 @@ type PluginShape =
| [PluginTarget, PluginOptions, undefined | string]
| [PluginTarget, PluginOptions]

export const getApiSideBabelPlugins = (
{ openTelemetry } = {
openTelemetry: false,
}
) => {
export const getApiSideBabelPlugins = ({
openTelemetry = false,
projectIsEsm = false,
} = {}) => {
const tsConfig = parseTypeScriptConfigFiles()

const plugins: Array<PluginShape | boolean> = [
Expand Down Expand Up @@ -128,7 +127,9 @@ export const getApiSideBabelPlugins = (
['babel-plugin-graphql-tag', undefined, 'rwjs-babel-graphql-tag'],
[
require('./plugins/babel-plugin-redwood-import-dir').default,
undefined,
{
projectIsEsm,
},
'rwjs-babel-glob-import-dir',
],
openTelemetry && [
Expand All @@ -150,7 +151,7 @@ export const getApiSideBabelConfigPath = () => {
}
}

export const getApiSideBabelOverrides = () => {
export const getApiSideBabelOverrides = ({ projectIsEsm = false } = {}) => {
const overrides = [
// Extract graphql options from the graphql function
// NOTE: this must come before the context wrapping
Expand All @@ -167,18 +168,23 @@ export const getApiSideBabelOverrides = () => {
// match */api/src/functions/*.js|ts
test: /.+api(?:[\\|/])src(?:[\\|/])functions(?:[\\|/]).+.(?:js|ts)$/,
plugins: [
require('./plugins/babel-plugin-redwood-context-wrapping').default,
[
require('./plugins/babel-plugin-redwood-context-wrapping').default,
{
projectIsEsm,
},
],
],
},
].filter(Boolean)
return overrides as TransformOptions[]
}

export const getApiSideDefaultBabelConfig = () => {
export const getApiSideDefaultBabelConfig = ({ projectIsEsm = false } = {}) => {
return {
presets: getApiSideBabelPresets(),
plugins: getApiSideBabelPlugins(),
overrides: getApiSideBabelOverrides(),
plugins: getApiSideBabelPlugins({ projectIsEsm }),
overrides: getApiSideBabelOverrides({ projectIsEsm }),
extends: getApiSideBabelConfigPath(),
babelrc: false,
ignore: ['node_modules'],
Expand All @@ -190,7 +196,9 @@ export const registerApiSideBabelHook = ({
plugins = [],
...rest
}: RegisterHookOptions = {}) => {
const defaultOptions = getApiSideDefaultBabelConfig()
const defaultOptions = getApiSideDefaultBabelConfig({
projectIsEsm: projectSideIsEsm('api'),
})

registerBabel({
...defaultOptions,
Expand All @@ -209,7 +217,9 @@ export const transformWithBabel = async (
plugins: TransformOptions['plugins']
) => {
const code = await fs.readFile(srcPath, 'utf-8')
const defaultOptions = getApiSideDefaultBabelConfig()
const defaultOptions = getApiSideDefaultBabelConfig({
projectIsEsm: projectSideIsEsm('api'),
})

const result = transformAsync(code, {
...defaultOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ function generateWrappedHandler(t: typeof types, isAsync: boolean) {
)
}

export default function ({ types: t }: { types: typeof types }): PluginObj {
export default function (
{ types: t }: { types: typeof types },
{ projectIsEsm = false }: { projectIsEsm?: boolean } = {}
): PluginObj {
return {
name: 'babel-plugin-redwood-context-wrapping',
visitor: {
Expand Down Expand Up @@ -97,7 +100,11 @@ export default function ({ types: t }: { types: typeof types }): PluginObj {
t.identifier('getAsyncStoreInstance')
),
],
t.stringLiteral('@redwoodjs/context/dist/store')
t.stringLiteral(
projectIsEsm
? '@redwoodjs/context/dist/store.js'
: '@redwoodjs/context/dist/store'
)
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import { importStatementPath } from '@redwoodjs/project-config'
* // services.nested_c = require('src/services/nested/c.js')
* ```
*/
export default function ({ types: t }: { types: typeof types }): PluginObj {
export default function (
{ types: t }: { types: typeof types },
{ projectIsEsm = false }: { projectIsEsm?: boolean } = {}
): PluginObj {
return {
name: 'babel-plugin-redwood-import-dir',
visitor: {
Expand Down Expand Up @@ -74,7 +77,11 @@ export default function ({ types: t }: { types: typeof types }): PluginObj {
t.identifier(importName + '_' + fpVarName)
),
],
t.stringLiteral(filePathWithoutExtension)
t.stringLiteral(
projectIsEsm
? `${filePathWithoutExtension}.js`
: filePathWithoutExtension
)
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ describe('upHandler', () => {
{
'redwood.toml': '',
api: {
'package.json': '{}',
dist: {
lib: {
'db.js': '',
Expand Down
3 changes: 3 additions & 0 deletions packages/graphql-server/build.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { build } from '@redwoodjs/framework-tools'

await build()
4 changes: 2 additions & 2 deletions packages/graphql-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
"dist"
],
"scripts": {
"build": "yarn build:js && yarn build:types",
"build:js": "babel src -d dist --extensions \".js,.jsx,.ts,.tsx\"",
"build": "tsx ./build.mts && yarn build:types",
"build:pack": "yarn pack -o redwoodjs-graphql-server.tgz",
"build:types": "tsc --build --verbose",
"build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"",
Expand Down Expand Up @@ -59,6 +58,7 @@
"aws-lambda": "1.0.7",
"jest": "29.7.0",
"jsonwebtoken": "9.0.2",
"tsx": "4.6.2",
"typescript": "5.3.3"
},
"gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1"
Expand Down
14 changes: 10 additions & 4 deletions packages/internal/src/build/api.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { BuildContext, BuildOptions, PluginBuild } from 'esbuild'
import { build, context } from 'esbuild'
import { remove } from 'fs-extra'
import fs from 'fs-extra'

import {
getApiSideBabelPlugins,
transformWithBabel,
} from '@redwoodjs/babel-config'
import { getConfig, getPaths } from '@redwoodjs/project-config'
import {
getConfig,
getPaths,
projectSideIsEsm,
} from '@redwoodjs/project-config'

import { findApiFiles } from '../files'

Expand All @@ -33,7 +37,7 @@ export const rebuildApi = async () => {

export const cleanApiBuild = async () => {
const rwjsPaths = getPaths()
return remove(rwjsPaths.api.dist)
return fs.remove(rwjsPaths.api.dist)
}

const runRwBabelTransformsPlugin = {
Expand All @@ -51,6 +55,7 @@ const runRwBabelTransformsPlugin = {
openTelemetry:
rwjsConfig.experimental.opentelemetry.enabled &&
rwjsConfig.experimental.opentelemetry.wrapApi,
projectIsEsm: projectSideIsEsm('api'),
})
)

Expand All @@ -72,13 +77,14 @@ export const transpileApi = async (files: string[]) => {

function getEsbuildOptions(files: string[]): BuildOptions {
const rwjsPaths = getPaths()
const format = projectSideIsEsm('api') ? 'esm' : 'cjs'

return {
absWorkingDir: rwjsPaths.api.base,
entryPoints: files,
platform: 'node',
target: 'node20',
format: 'cjs',
format,
allowOverwrite: true,
bundle: false,
plugins: [runRwBabelTransformsPlugin],
Expand Down
Loading

0 comments on commit 1bc3ecd

Please sign in to comment.