Skip to content

Commit

Permalink
Convert @redwoodjs/web to ESM/CJS dual package (#10868)
Browse files Browse the repository at this point in the history
  • Loading branch information
dac09 authored Jun 28, 2024
1 parent 5ba9ed2 commit 3ff2c9e
Show file tree
Hide file tree
Showing 35 changed files with 823 additions and 198 deletions.
2 changes: 1 addition & 1 deletion packages/core/config/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ module.exports = (webpackEnv) => {
*/
app:
redwoodPaths.web.index ||
require.resolve('@redwoodjs/web/dist/entry/index.js'),
require.resolve('@redwoodjs/web/webpackEntry'),
},
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.jsx', '.ts', '.tsx', '.json'],
Expand Down
17 changes: 17 additions & 0 deletions packages/internal/src/generate/typeDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const generateTypeDefs = async () => {
...generateTypeDefScenarios(),
...generateTypeDefTestMocks(),
...generateStubStorybookTypes(),
...generateViteClientTypesDirective(),
...gqlApiTypeDefFiles,
...gqlWebTypeDefFiles,
],
Expand Down Expand Up @@ -378,6 +379,22 @@ export const generateTypeDefGlobImports = () => {
export const generateTypeDefGlobalContext = () => {
return writeTypeDefIncludeFile('api-globalContext.d.ts.template')
}
/**
* Typescript does not preserve triple slash directives when outputting js or d.ts files.
* This is a work around so that *.svg, *.png, etc. imports have types.
*/
export const generateViteClientTypesDirective = () => {
const viteClientDirective = `/// <reference types="vite/client" />`
const redwoodProjectPaths = getPaths()

const viteClientDirectivePath = path.join(
redwoodProjectPaths.generated.types.includes,
'web-vite-client.d.ts',
)
fs.writeFileSync(viteClientDirectivePath, viteClientDirective)

return [viteClientDirectivePath]
}

function generateStubStorybookTypes() {
const stubStorybookTypesFileContent = `\
Expand Down
2 changes: 1 addition & 1 deletion packages/prerender/src/graphql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { print } from 'graphql'

import { getConfig, getPaths } from '@redwoodjs/project-config'
// @MARK: have to do this, otherwise rwjs/web is loaded before shims
import { getOperationName } from '@redwoodjs/web/dist/graphql'
import { getOperationName } from '@redwoodjs/web/dist/graphql.js'

import { GqlHandlerImportError } from '../errors'

Expand Down
2 changes: 1 addition & 1 deletion packages/testing/config/jest/web/jest-preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ module.exports = {
NODE_MODULES_PATH,
'@redwoodjs/testing/dist/web/MockRouter.js',
),
'^@redwoodjs/web$': path.join(NODE_MODULES_PATH, '@redwoodjs/web'),
'^@redwoodjs/web$': path.join(NODE_MODULES_PATH, '@redwoodjs/web/dist/cjs'),

// This allows us to mock `createAuthentication` which is used by auth
// clients, which in turn lets us mock `useAuth` in tests
Expand Down
8 changes: 8 additions & 0 deletions packages/testing/config/storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ const baseConfig = {
sbConfig.resolve.alias['@redwoodjs/auth$'] = require.resolve(
'@redwoodjs/testing/dist/web/mockAuth.js',
)

// Force loading the ESM version of ApolloProvider in Storybook
// I'm unsure why storybook-webpack does not work with the CJS version
// All other cases are fine with the CJS import.
sbConfig.resolve.alias['@redwoodjs/web/apollo$'] = require.resolve(
'@redwoodjs/web/forceEsmApollo',
)

sbConfig.resolve.alias['~__REDWOOD__USER_ROUTES_FOR_MOCK'] =
redwoodProjectPaths.web.routes
sbConfig.resolve.alias['~__REDWOOD__USER_WEB_SRC'] =
Expand Down
1 change: 1 addition & 0 deletions packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"react-server-dom-webpack": "19.0.0-beta-04b058868c-20240508",
"vite": "5.3.1",
"vite-plugin-cjs-interop": "2.1.1",
"vite-plugin-node-polyfills": "^0.22.0",
"yargs-parser": "21.1.1"
},
"devDependencies": {
Expand Down
8 changes: 7 additions & 1 deletion packages/vite/src/devFeServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@ async function createServer() {
configFile: rwPaths.web.viteConfig,
plugins: [
cjsInterop({
dependencies: ['@redwoodjs/**'],
dependencies: [
// Skip ESM modules: rwjs/auth, rwjs/web
'@redwoodjs/forms',
'@redwoodjs/prerender/*',
'@redwoodjs/router',
'@redwoodjs/auth-*',
],
}),
rscEnabled && rscRoutesAutoLoader(),
],
Expand Down
12 changes: 12 additions & 0 deletions packages/vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'path'
import react from '@vitejs/plugin-react'
import type { PluginOption } from 'vite'
import { normalizePath } from 'vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills'

import { getWebSideDefaultBabelConfig } from '@redwoodjs/babel-config'
import { getConfig, getPaths } from '@redwoodjs/project-config'
Expand Down Expand Up @@ -156,5 +157,16 @@ export default function redwoodPluginVite(): PluginOption[] {
}),
},
}),
// Only include the Buffer polyfill for non-rsc dev, for DevFatalErrorPage
// Including the polyfill plugin in any form in RSC breaks
!rscEnabled && {
...nodePolyfills({
include: ['buffer'],
globals: {
Buffer: true,
},
}),
apply: 'serve',
},
]
}
14 changes: 12 additions & 2 deletions packages/vite/src/rsc/rscBuildForSsr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,16 @@ export async function rscBuildForSsr({
],
},
plugins: [
cjsInterop({ dependencies: ['@redwoodjs/**'] }),
cjsInterop({
dependencies: [
// Skip ESM modules: rwjs/auth, rwjs/web
'@redwoodjs/forms',
'@redwoodjs/prerender/*',
'@redwoodjs/router',
'@redwoodjs/router/*',
'@redwoodjs/auth-*',
],
}),
rscRoutesAutoLoader(),
rscSsrRouterImport(),
],
Expand All @@ -84,10 +93,11 @@ export async function rscBuildForSsr({
// for the client-only components. They get loaded once the page is
// rendered
...clientEntryFiles,
// These import redirections are so that we don't bundle multiple versions of react
__rwjs__react: 'react',
__rwjs__location: '@redwoodjs/router/dist/location',
__rwjs__server_auth_provider: '@redwoodjs/auth/ServerAuthProvider',
__rwjs__server_inject: '@redwoodjs/web/dist/components/ServerInject',
__rwjs__server_inject: '@redwoodjs/web/serverInject',
'__rwjs__rsdw-client': 'react-server-dom-webpack/client.edge',
// TODO (RSC): add __rwjs__ prefix to the entry below
'rd-server': 'react-dom/server.edge',
Expand Down
8 changes: 7 additions & 1 deletion packages/vite/src/streaming/buildForStreamingServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ export async function buildForStreamingServer({
configFile: rwPaths.web.viteConfig,
plugins: [
cjsInterop({
dependencies: ['@redwoodjs/**'],
dependencies: [
// Skip ESM modules: rwjs/auth, rwjs/web
'@redwoodjs/forms',
'@redwoodjs/prerender/*',
'@redwoodjs/router',
'@redwoodjs/auth-*',
],
}),
],
build: {
Expand Down
8 changes: 6 additions & 2 deletions packages/vite/src/streaming/streamHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,16 @@ export async function reactRenderToStreamResponse(
const { createElement }: React = rscEnabled
? await importModule('__rwjs__react')
: await import('react')

const {
createInjector,
ServerHtmlProvider,
ServerInjectedHtml,
}: ServerInjectType = rscEnabled
? await importModule('__rwjs__server_inject')
: await import('@redwoodjs/web/dist/components/ServerInject.js')
: // @ts-expect-error this is defined in packages/web/package.json exports.
// This package just doesn't have moduleResolution configured
await import('@redwoodjs/web/serverInject')
const { renderToString }: RDServerType = rscEnabled
? await importModule('rd-server')
: await import('react-dom/server')
Expand Down Expand Up @@ -322,7 +325,8 @@ export async function importModule(
} else if (mod === '__rwjs__server_auth_provider') {
return await import(ServerAuthProviderPath)
} else if (mod === '__rwjs__server_inject') {
return (await import(ServerInjectPath)).default
// Don't need default because rwjs/web is now ESM
return await import(ServerInjectPath)
}

throw new Error('Unknown module ' + mod)
Expand Down
2 changes: 1 addition & 1 deletion packages/web/apollo/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* eslint-env es6, commonjs */
module.exports = require('../dist/apollo')
module.exports = require('../dist/cjs/apollo/index.js')
32 changes: 17 additions & 15 deletions packages/web/build.mts
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
// import { writeFileSync } from 'node:fs'

import {
build,
defaultBuildOptions,
defaultIgnorePatterns,
} from '@redwoodjs/framework-tools'
import { writeFileSync } from 'node:fs'

// CJS build
/**
* Notes:
* - we don't build the webpack entry point in CJS, because it produces a double wrapped module
* instead we use the ESM version (see ./webpackEntry in package.json). The double wrapping happens
* when you set type: module in package.json, and occurs on the App & Routes import from the project.
* - we build bins in CJS, until projects fully switch to ESM (or we produce .mts files) this is probably
* the better option
*/
await build({
entryPointOptions: {
ignore: [...defaultIgnorePatterns],
ignore: [...defaultIgnorePatterns, 'src/__typetests__/**', 'src/entry/**'],
},
buildOptions: {
...defaultBuildOptions,
// ⭐ No special tsconfig here
// outdir: 'dist/cjs', DONT DO THIS JUST YET
outdir: 'dist',
// ⭐ No special build tsconfig in this package
outdir: 'dist/cjs',
packages: 'external',
},
})

/** THIS IS IN PART 2 ~ making this a dual module
Will enable in follow up PR
ESM build
// ESM build
await build({
entryPointOptions: {
ignore: [...defaultIgnorePatterns, 'src/entry/**'],
// @NOTE: building the cjs bins only...
// I haven't tried esm bins yet...
ignore: [...defaultIgnorePatterns, 'src/bins/**', 'src/__typetests__/**'],
},
buildOptions: {
...defaultBuildOptions,
// ⭐ No special tsconfig here
// tsconfig: 'tsconfig.build.json',
// ⭐ No special build tsconfig in this package
format: 'esm',
packages: 'external',
},
Expand All @@ -44,5 +48,3 @@ writeFileSync('dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }))
// Place a package.json file with `type: module` in the dist/esm folder so that
// all .js files are treated as ES Module files.
writeFileSync('dist/package.json', JSON.stringify({ type: 'module' }))
*/
71 changes: 62 additions & 9 deletions packages/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@redwoodjs/web",
"version": "7.0.0",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/redwoodjs/redwood.git",
Expand All @@ -10,14 +11,66 @@
"main": "./dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"cross-env": "./dist/bins/cross-env.js",
"msw": "./dist/bins/msw.js",
"redwood": "./dist/bins/redwood.js",
"rw": "./dist/bins/redwood.js",
"rwfw": "./dist/bins/rwfw.js",
"storybook": "./dist/bins/storybook.js",
"tsc": "./dist/bins/tsc.js",
"webpack": "./dist/bins/webpack.js"
"cross-env": "./dist/cjs/bins/cross-env.js",
"msw": "./dist/cjs/bins/msw.js",
"redwood": "./dist/cjs/bins/redwood.js",
"rw": "./dist/cjs/bins/redwood.js",
"rwfw": "./dist/cjs/bins/rwfw.js",
"storybook": "./dist/cjs/bins/storybook.js",
"tsc": "./dist/cjs/bins/tsc.js",
"webpack": "./dist/cjs/bins/webpack.js"
},
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/cjs/index.d.ts",
"default": "./dist/cjs/index.js"
}
},
"./serverInject": {
"require": "./dist/cjs/components/ServerInject.js",
"import": "./dist/components/ServerInject.js",
"types": "./dist/components/ServerInject.d.ts",
"default": "./dist/components/ServerInject.js"
},
"./dist/components/*": {
"require": "./dist/cjs/components/*.js",
"import": "./dist/components/*.js"
},
"./dist/apollo/suspense": {
"require": "./dist/cjs/apollo/suspense.js",
"import": "./dist/apollo/suspense.js"
},
"./dist/apollo/sseLink": {
"require": "./dist/cjs/apollo/sseLink.js",
"import": "./dist/apollo/sseLink.js"
},
"./dist/graphql.js": {
"require": "./dist/cjs/graphql.js",
"import": "./dist/graphql.js"
},
"./webpackEntry": {
"default": "./dist/entry/index.js"
},
"./toast": {
"require": "./dist/cjs/toast/index.js",
"import": "./dist/toast/index.js",
"types": "./dist/toast/index.d.ts"
},
"./apollo": {
"require": "./dist/cjs/apollo/index.js",
"import": "./dist/apollo/index.js",
"types": "./dist/apollo/index.d.ts",
"default": "./dist/cjs/apollo/index.js"
},
"./forceEsmApollo": {
"require": "./dist/apollo/index.js",
"import": "./dist/apollo/index.js"
}
},
"files": [
"dist",
Expand All @@ -28,7 +81,7 @@
"scripts": {
"build": "tsx ./build.mts && yarn build:types",
"build:pack": "yarn pack -o redwoodjs-web.tgz",
"build:types": "tsc --build --verbose tsconfig.json",
"build:types": "tsc --build --verbose ./tsconfig.json ./tsconfig.types-cjs.json",
"build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"",
"prepublishOnly": "NODE_ENV=production yarn build",
"test": "vitest run",
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/__typetests__/cellProps.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// These are normally auto-imported by babel
import React from 'react'

import gql from 'graphql-tag'
import { gql } from 'graphql-tag'
import { describe, expect, test } from 'tstyche'

import type { CellProps, CellSuccessProps } from '@redwoodjs/web'
Expand Down
8 changes: 5 additions & 3 deletions packages/web/src/apollo/fragmentRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as apolloClient from '@apollo/client'
import type { UseFragmentResult } from '@apollo/client'
import type { FragmentRegistryAPI } from '@apollo/client/cache'
import { createFragmentRegistry } from '@apollo/client/cache'
import { getFragmentDefinitions } from '@apollo/client/utilities'
// @ts-expect-error Force import cjs module
import { createFragmentRegistry } from '@apollo/client/cache/cache.cjs'
import type { FragmentRegistryAPI } from '@apollo/client/cache/index.js'
// @ts-expect-error Force import cjs module
import { getFragmentDefinitions } from '@apollo/client/utilities/utilities.cjs'
import type { DocumentNode } from 'graphql'

export type FragmentIdentifier = string | number
Expand Down
Loading

0 comments on commit 3ff2c9e

Please sign in to comment.