Skip to content

Commit

Permalink
fix: vite deps optimizer usage (#26)
Browse files Browse the repository at this point in the history
With this PR the framework in development mode relies on Vite's
dependency optimizer in a cleaner and simpler way to handle CommonJS
dependencies.

Adds a new Chakra UI example to have another third-party library usage
as a test case for module resolution.

Cleans up some inline Vite / Rollup plugin code.

Another possible enhancement to fix issues around third-party libraries,
related to issues:
- Mantine / use client not respected
#20
- MUI 6.x / React__namespace.createContext is not a function
#22
  • Loading branch information
lazarv authored Sep 11, 2024
1 parent da250e2 commit 6282ca0
Show file tree
Hide file tree
Showing 15 changed files with 1,577 additions and 167 deletions.
4 changes: 4 additions & 0 deletions examples/chakra-ui/app/Chakra.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"use client";

import { Button, Spinner } from "@chakra-ui/react";
export { Button, Spinner };
7 changes: 7 additions & 0 deletions examples/chakra-ui/app/Providers.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import { ChakraProvider } from "@chakra-ui/react";

export default function Providers({ children }) {
return <ChakraProvider>{children}</ChakraProvider>;
}
11 changes: 11 additions & 0 deletions examples/chakra-ui/app/layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Providers from "./Providers";

export default function Layout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
17 changes: 17 additions & 0 deletions examples/chakra-ui/app/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Suspense } from "react";

import { Button, Spinner } from "./Chakra";

async function AsyncComponent() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return null;
}

export default function Page() {
return (
<Suspense fallback={<Spinner />}>
<AsyncComponent />
<Button>Hello Chakra UI!</Button>
</Suspense>
);
}
21 changes: 21 additions & 0 deletions examples/chakra-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@lazarv/react-server-example-chakra-ui",
"private": true,
"description": "@lazarv/react-server Chakra UI example application",
"scripts": {
"dev": "react-server",
"build": "react-server build",
"start": "react-server start"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@lazarv/react-server": "workspace:^",
"@lazarv/react-server-router": "workspace:^",
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"framer-motion": "^11.5.4"
}
}
22 changes: 22 additions & 0 deletions examples/chakra-ui/react-server.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default {
root: "app",
public: "public",
page: {
include: ["**/page.jsx"],
},
layout: {
include: ["**/layout.jsx"],
},
build: {
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("@chakra-ui/react")) {
return "@chakra-ui/react";
}
},
},
},
},
};
3 changes: 0 additions & 3 deletions examples/mui/react-server.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ export default {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("@emotion/react")) {
return "@emotion/react";
}
if (id.includes("@mui/")) {
return "@mui/material";
}
Expand Down
1 change: 0 additions & 1 deletion packages/react-server/lib/build/client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { mkdir, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { createHash } from "node:crypto";

import replace from "@rollup/plugin-replace";
import colors from "picocolors";
Expand Down
53 changes: 2 additions & 51 deletions packages/react-server/lib/build/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import resolveWorkspace from "../plugins/resolve-workspace.mjs";
import rollupUseClient from "../plugins/use-client.mjs";
import rollupUseServerInline from "../plugins/use-server-inline.mjs";
import rollupUseServer from "../plugins/use-server.mjs";
import rootModule from "../plugins/root-module.mjs";
import * as sys from "../sys.mjs";
import {
filterOutVitePluginReact,
Expand Down Expand Up @@ -180,57 +181,7 @@ export default async function serverBuild(root, options) {
rollupUseClient("server", clientManifest),
rollupUseServer("rsc", serverManifest),
rollupUseServerInline(serverManifest),
{
name: "react-server:root-module",
transform(code, id) {
if (!root || root?.startsWith("virtual:")) return null;
const [module, name] = root.split("#");
const rootModule = __require.resolve(module, { paths: [cwd] });

if (id === rootModule) {
const ast = this.parse(code, { sourceType: "module" });

const defaultExport = ast.body.find(
(node) => node.type === "ExportDefaultDeclaration"
);
const namedExports = ast.body
.filter(
(node) =>
node.type === "ExportNamedDeclaration" && node.declaration
)
.map((node) => node.declaration.id.name);
const allExports = ast.body
.filter(
(node) =>
node.type === "ExportNamedDeclaration" &&
node.specifiers.length > 0
)
.flatMap((node) => node.specifiers)
.map((node) => node.exported.name);

const rootName = name ?? "default";
if (
(rootName === "default" &&
!defaultExport &&
!allExports?.find((name) => name === "default")) ||
(rootName !== "default" &&
!namedExports.find((name) => name === rootName) &&
!allExports?.find((name) => name === rootName))
) {
throw new Error(
`Module "${rootModule}" does not export "${rootName}"`
);
}

if (name && name !== "default") {
return {
code: `${code}\nexport { ${name} as default };`,
map: null,
};
}
}
},
},
rootModule(root),
...(config.build?.rollupOptions?.plugins ?? []),
],
},
Expand Down
15 changes: 6 additions & 9 deletions packages/react-server/lib/dev/create-logger.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,16 @@ export default function createLogger(level = "info", options) {
...["info", "warn", "warnOnce"].reduce((newLogger, command) => {
newLogger[command] = (...args) => {
const [msg, ...rest] = args;
const lowerCaseMsg = msg.toLowerCase();
// omit vite warnings on prop-types and react-is
const options = rest?.[rest?.length - 1];
// omit vite warnings on possible dep optimizer incompatibility
if (
lowerCaseMsg.includes("warning: rewrote prop-types") ||
lowerCaseMsg.includes("warning: rewrote react-is") ||
(lowerCaseMsg.includes("cannot optimize dependency:") &&
lowerCaseMsg.includes("prop-types")) ||
(lowerCaseMsg.includes("cannot optimize dependency:") &&
lowerCaseMsg.includes("react-is"))
command === "warn" &&
msg.includes(
"dependency might be incompatible with the dep optimizer"
)
) {
return;
}
const options = rest?.[rest?.length - 1];
logger[command](
repeatMessage(
typeof msg !== "string"
Expand Down
127 changes: 24 additions & 103 deletions packages/react-server/lib/dev/create-server.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { rm } from "node:fs/promises";
import { createRequire, register } from "node:module";
import { join, relative } from "node:path";
import { pathToFileURL } from "node:url";
Expand Down Expand Up @@ -38,6 +39,8 @@ import staticWatchHandler from "../handlers/static-watch.mjs";
import trailingSlashHandler from "../handlers/trailing-slash.mjs";
import { alias, moduleAliases } from "../loader/module-alias.mjs";
import { applyAlias } from "../loader/utils.mjs";
import asset from "../plugins/asset.mjs";
import optimizeDeps from "../plugins/optimize-deps.mjs";
import reactServerEval from "../plugins/react-server-eval.mjs";
import reactServerRuntime from "../plugins/react-server-runtime.mjs";
import resolveWorkspace from "../plugins/resolve-workspace.mjs";
Expand All @@ -47,12 +50,7 @@ import useServerInline from "../plugins/use-server-inline.mjs";
import * as sys from "../sys.mjs";
import { replaceError } from "../utils/error.mjs";
import merge from "../utils/merge.mjs";
import {
bareImportRE,
findPackageRoot,
isModule,
tryStat,
} from "../utils/module.mjs";
import { bareImportRE, findPackageRoot, tryStat } from "../utils/module.mjs";
import {
filterOutVitePluginReact,
userOrBuiltInVitePluginReact,
Expand Down Expand Up @@ -156,18 +154,8 @@ export default async function createServer(root, options) {
useServer(),
useServerInline(),
...filterOutVitePluginReact(config.plugins),
{
name: "react-server:asset",
transform(code) {
if (code.startsWith(`export default "/@fs${cwd}`)) {
return code.replace(
`export default "/@fs${cwd}`,
`export default "`
);
}
return null;
},
},
asset(),
optimizeDeps(),
],
cacheDir: join(cwd, options.outDir, ".cache/client"),
resolve: {
Expand All @@ -179,26 +167,6 @@ export default async function createServer(root, options) {
find: /^@lazarv\/react-server\/client$/,
replacement: join(sys.rootDir, "client"),
},
// compatibility entries
{
find: "use-sync-external-store/shim/with-selector.js",
replacement: join(
sys.rootDir,
"use-sync-external-store/shim/with-selector.mjs"
),
},
{
find: "hoist-non-react-statics",
replacement: "hoist-non-react-statics/src/index.js",
},
{
find: "prop-types",
replacement: "prop-types",
},
{
find: "react-is",
replacement: "react-is",
},
...(config.resolve?.alias ?? []),
],
noExternal: [bareImportRE],
Expand Down Expand Up @@ -329,24 +297,13 @@ export default async function createServer(root, options) {
? config.vite(devServerConfig) ?? devServerConfig
: merge(devServerConfig, config.vite);

const tryToOptimize = (dep) => {
if (options.force) {
try {
__require.resolve(dep, { paths: [cwd] });
viteConfig.optimizeDeps.include = [
...(viteConfig.optimizeDeps.include ?? []),
dep,
];
viteConfig.resolve.external = [
...(viteConfig.resolve.external ?? []),
dep,
];
} catch (e) {
// no prop-types
await rm(viteConfig.cacheDir, { recursive: true });
} catch {
// ignore
}
};

tryToOptimize("prop-types");
tryToOptimize("react-is");
}

const viteDevServer = await createViteDevServer(viteConfig);
viteDevServer.environments.client.hot = viteDevServer.ws;
Expand Down Expand Up @@ -391,63 +348,27 @@ export default async function createServer(root, options) {
);

const reactServerAlias = moduleAliases("react-server");

const originalDirectRequest = moduleRunner.directRequest;
moduleRunner.directRequest = async (id, mod, callstack) => {
try {
return await originalDirectRequest.call(moduleRunner, id, mod, callstack);
} catch (e) {
return {
externalize: id,
};
}
};

const originalFetchModule = moduleRunner.transport.fetchModule;
moduleRunner.transport.fetchModule = async (specifier, parentId, meta) => {
const alias = applyAlias(reactServerAlias, specifier);
const id = tryStat(alias) ? alias : specifier;
if (alias !== specifier && tryStat(alias)) {
return {
externalize: specifier,
type: "commonjs",
};
}
try {
const resolved = await originalFetchModule.call(
return await originalFetchModule.call(
moduleRunner.transport,
id,
specifier,
parentId,
meta
);
if (!resolved.file || !/\.[mc]?js$/.test(resolved.file)) {
return resolved;
}
if (
resolved.file &&
/node_modules/.test(resolved.file) &&
!isModule(resolved.file)
) {
const externalized = {
externalize: resolved.file,
type: "commonjs",
};
return externalized;
}
return resolved;
} catch (e) {
try {
const resolved = import.meta.resolve(id);
const resolvedIsModule = isModule(resolved);
if (resolvedIsModule) {
return {
externalize: resolved,
type: "module",
};
}
return {
externalize: id,
type: "commonjs",
};
} catch (e) {
return {
externalize: id,
};
}
} catch {
return {
externalize: specifier,
type: "module",
};
}
};

Expand Down
15 changes: 15 additions & 0 deletions packages/react-server/lib/plugins/asset.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as sys from "../sys.mjs";

const cwd = sys.cwd();

export default function asset() {
return {
name: "react-server:asset",
transform(code) {
if (code.startsWith(`export default "/@fs${cwd}`)) {
return code.replace(`export default "/@fs${cwd}`, `export default "`);
}
return null;
},
};
}
Loading

0 comments on commit 6282ca0

Please sign in to comment.