diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d8c08f64..ac7b872d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,7 +15,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- - run: npm i -g pnpm && pnpm i --ignore-scripts
+ - run: npm i -g pnpm && pnpm i
name: Install dependencies
- run: pnpm build --filter esbuild-plugin-react18-example
name: build example app to run tests
diff --git a/README.md b/README.md
index 520acd62..c6dad050 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# esbuild-plugin-react18 [](https://github.com/mayank1513/esbuild-plugin-react18/actions/workflows/test.yml) [](https://www.npmjs.com/package/esbuild-plugin-react18) [](https://www.npmjs.com/package/esbuild-plugin-react18) 
+# esbuild-plugin-react18 [](https://github.com/mayank1513/esbuild-plugin-react18/actions/workflows/test.yml) [](https://codecov.io/gh/mayank1513/esbuild-plugin-react18) [](https://www.npmjs.com/package/esbuild-plugin-react18) [](https://www.npmjs.com/package/esbuild-plugin-react18) 
diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json
index f5955376..e334ba72 100644
--- a/examples/nextjs/package.json
+++ b/examples/nextjs/package.json
@@ -20,6 +20,7 @@
"@types/node": "^20.6.3",
"@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7",
+ "esbuild-plugin-react18": "workspace:^",
"eslint-config-custom": "workspace:*",
"tsconfig": "workspace:*",
"typescript": "^5.2.2"
diff --git a/examples/vite/package.json b/examples/vite/package.json
index e903d76e..76022c14 100644
--- a/examples/vite/package.json
+++ b/examples/vite/package.json
@@ -22,6 +22,7 @@
"@typescript-eslint/eslint-plugin": "^6.7.2",
"@typescript-eslint/parser": "^6.7.2",
"@vitejs/plugin-react-swc": "^3.3.2",
+ "esbuild-plugin-react18": "workspace:^",
"eslint": "^8.50.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
diff --git a/packages/esbuild-plugin-react18-example/src/client/star-me/ignore-me.ts b/packages/esbuild-plugin-react18-example/src/client/star-me/ignore-me.ts
new file mode 100644
index 00000000..2133a155
--- /dev/null
+++ b/packages/esbuild-plugin-react18-example/src/client/star-me/ignore-me.ts
@@ -0,0 +1,5 @@
+// ignore-me based on content
+
+/** ignore with content pattern */
+
+export const IamIgnored = true;
diff --git a/packages/esbuild-plugin-react18-example/src/server/constants.ts b/packages/esbuild-plugin-react18-example/src/server/constants.ts
new file mode 100644
index 00000000..b231c4a2
--- /dev/null
+++ b/packages/esbuild-plugin-react18-example/src/server/constants.ts
@@ -0,0 +1,3 @@
+/** we will replace default colors to test sourceReplacePatterns */
+export const defaultBgColor = "#aaa";
+export const defaultColor = "#555";
diff --git a/packages/esbuild-plugin-react18-example/src/server/fork-me/fork-me.tsx b/packages/esbuild-plugin-react18-example/src/server/fork-me/fork-me.tsx
index 33676886..0284ca3a 100644
--- a/packages/esbuild-plugin-react18-example/src/server/fork-me/fork-me.tsx
+++ b/packages/esbuild-plugin-react18-example/src/server/fork-me/fork-me.tsx
@@ -1,5 +1,6 @@
import * as React from "react";
import cssClasses from "./fork-me.module.css";
+import { defaultBgColor, defaultColor } from "../constants";
interface ForkMeProps {
gitHubUrl: string;
@@ -29,8 +30,8 @@ export function ForkMe({
}: ForkMeProps) {
const w = (Number.isNaN(Number(width)) ? width : `${width}px`) || "15em";
const h = (Number.isNaN(Number(height)) ? height : `${height}px`) || "35px";
- const bgC = bgColor || "#aaa";
- const tC = textColor || "#555";
+ const bgC = bgColor || defaultBgColor;
+ const tC = textColor || defaultColor;
const style = {
"--w": w,
"--h": h,
diff --git a/packages/esbuild-plugin-react18/CHANGELOG.md b/packages/esbuild-plugin-react18/CHANGELOG.md
index ac75acde..7c95845b 100644
--- a/packages/esbuild-plugin-react18/CHANGELOG.md
+++ b/packages/esbuild-plugin-react18/CHANGELOG.md
@@ -1,5 +1,13 @@
# esbuild-plugin-react18
+## 0.0.3
+
+### Patch Changes
+
+- e5d3a58: remove data-testid as last item in sourceReplacePatterns. This will reduce the onLoad conflicts significantly as this pattern matches all the js, ts, jsx and tsx files.
+- Fix buildReplacePatterns to work on all esbuild setups
+- e186209: Fix ignorePatterns with contentPatterns
+
## 0.0.2
### Patch Changes
diff --git a/packages/esbuild-plugin-react18/__tests__/buildReplacePatterns.test.ts b/packages/esbuild-plugin-react18/__tests__/buildReplacePatterns.test.ts
new file mode 100644
index 00000000..29f36305
--- /dev/null
+++ b/packages/esbuild-plugin-react18/__tests__/buildReplacePatterns.test.ts
@@ -0,0 +1,56 @@
+import fs from "node:fs";
+import path from "node:path";
+import { describe, test, beforeAll } from "vitest";
+import esbuild from "esbuild";
+import react18Plugin from "../src";
+import glob from "tiny-glob";
+
+/**
+ * buildReplacePatterns could be very helpful in removing unnecessary comments introduced while bundling from other libraries
+ */
+describe.concurrent("Test plugin with ignorePatterns -- without content pattern", async () => {
+ const outDir = "build-replace-patterns";
+ const exampleBuildDir = path.resolve(process.cwd(), "dist", outDir);
+ try {
+ fs.unlinkSync(path.resolve(exampleBuildDir));
+ } catch {}
+ beforeAll(async () => {
+ await esbuild.build({
+ format: "cjs",
+ target: "es2019",
+ sourcemap: false,
+ bundle: true,
+ minify: true,
+ plugins: [
+ react18Plugin({
+ buildReplacePatterns: [
+ {
+ pathPattern: /constants/,
+ replaceParams: [
+ { pattern: /aaa/, substitute: "3c3c3c" },
+ { pattern: /#555/, substitute: "#ccc" },
+ ],
+ },
+ ],
+ }),
+ ],
+ entryPoints: await glob("../esbuild-plugin-react18-example/src/**/*.*"),
+ publicPath: "https://my.domain/static/",
+ external: ["react", "react-dom"],
+ outdir: "./dist/" + outDir,
+ });
+ });
+
+ test(`"use client"; directive should be present in client components`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "client", "index.js"), "utf-8");
+ expect(/^"use client";\n/m.test(text)).toBe(true);
+ });
+ test(`"use client"; directive should not be present in server components`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "server", "index.js"), "utf-8");
+ expect(/^"use client";\n/m.test(text)).toBe(false);
+ });
+ test(`defaultBgColor should be "#3c3c3c" and defaultColor should be "#ccc"`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "server", "constants.js"), "utf-8");
+ expect(text.includes("3c3c3c")).toBe(true);
+ });
+});
diff --git a/packages/esbuild-plugin-react18/__tests__/defaultOptions.test.ts b/packages/esbuild-plugin-react18/__tests__/defaultOptions.test.ts
index 888587b2..e1620a3c 100644
--- a/packages/esbuild-plugin-react18/__tests__/defaultOptions.test.ts
+++ b/packages/esbuild-plugin-react18/__tests__/defaultOptions.test.ts
@@ -1,15 +1,15 @@
import fs from "node:fs";
import path from "node:path";
-import { describe, test } from "vitest";
+import { describe, test, beforeAll } from "vitest";
-describe.concurrent("Test plugin with default options", () => {
+/** testing tsup example - make sure it is build before running this test suit */
+describe.concurrent("Test plugin with default options in example build with tsup", () => {
const exampleBuildDir = path.resolve(
process.cwd(),
"..",
"esbuild-plugin-react18-example",
"dist",
);
- console.log({ exampleBuildDir });
test(`"use client"; directive should be present in client components`, ({ expect }) => {
const text = fs.readFileSync(path.resolve(exampleBuildDir, "client", "index.js"), "utf-8");
expect(/^"use client";\n/m.test(text)).toBe(true);
@@ -18,4 +18,11 @@ describe.concurrent("Test plugin with default options", () => {
const text = fs.readFileSync(path.resolve(exampleBuildDir, "server", "index.js"), "utf-8");
expect(/^"use client";\n/m.test(text)).toBe(false);
});
+ test(`should not contain data-testid`, ({ expect }) => {
+ const text = fs.readFileSync(
+ path.resolve(exampleBuildDir, "client", "star-me", "star-me.js"),
+ "utf-8",
+ );
+ expect(/data-testid/.test(text)).toBe(false);
+ });
});
diff --git a/packages/esbuild-plugin-react18/__tests__/ignorePatterns.test.ts b/packages/esbuild-plugin-react18/__tests__/ignorePatterns.test.ts
new file mode 100644
index 00000000..9f9ae11e
--- /dev/null
+++ b/packages/esbuild-plugin-react18/__tests__/ignorePatterns.test.ts
@@ -0,0 +1,79 @@
+import fs from "node:fs";
+import path from "node:path";
+import { describe, test, beforeAll } from "vitest";
+import esbuild from "esbuild";
+import react18Plugin from "../src";
+import glob from "tiny-glob";
+
+describe.concurrent("Test plugin with ignorePatterns -- without content pattern", async () => {
+ const outDir = "ignore-patterns-0";
+ const exampleBuildDir = path.resolve(process.cwd(), "dist", outDir);
+ try {
+ fs.unlinkSync(path.resolve(exampleBuildDir));
+ } catch {}
+ beforeAll(async () => {
+ await esbuild.build({
+ format: "cjs",
+ target: "es2019",
+ sourcemap: false,
+ bundle: true,
+ minify: true,
+ plugins: [react18Plugin({ ignorePatterns: [{ pathPattern: /star-me/ }] })],
+ entryPoints: await glob("../esbuild-plugin-react18-example/src/**/*.*"),
+ publicPath: "https://my.domain/static/",
+ external: ["react", "react-dom"],
+ outdir: "./dist/" + outDir,
+ });
+ });
+
+ test(`"use client"; directive should be present in client components`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "client", "index.js"), "utf-8");
+ expect(/^"use client";\n/m.test(text)).toBe(true);
+ });
+ test(`"use client"; directive should not be present in server components`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "server", "index.js"), "utf-8");
+ expect(/^"use client";\n/m.test(text)).toBe(false);
+ });
+ test(`star-me.tsx file should not exist`, ({ expect }) => {
+ expect(fs.existsSync(path.resolve(exampleBuildDir, "client", "star-me"))).toBe(false);
+ });
+});
+
+/**
+ * When content pattern is provided only the ignorePattern files having content matching the content pattern will be removed
+ */
+describe.concurrent("Test plugin with ignorePatterns with content pattern", async () => {
+ const outDir = "ignore-patterns-1";
+ beforeAll(async () => {
+ await esbuild.build({
+ format: "cjs",
+ target: "es2019",
+ sourcemap: false,
+ bundle: true,
+ minify: true,
+ plugins: [
+ react18Plugin({
+ ignorePatterns: [{ pathPattern: /star-me/, contentPatterns: [/ignore-me/] }],
+ }),
+ ],
+ entryPoints: await glob("../esbuild-plugin-react18-example/src/**/*.*"),
+ publicPath: "https://my.domain/static/",
+ external: ["react", "react-dom"],
+ outdir: "./dist/" + outDir,
+ });
+ });
+
+ const exampleBuildDir = path.resolve(process.cwd(), "dist", outDir);
+ test(`star-me.tsx file should exist`, ({ expect }) => {
+ expect(fs.existsSync(path.resolve(exampleBuildDir, "client", "star-me", "star-me.js"))).toBe(
+ true,
+ );
+ });
+ test(`ignore-me.ts file should not exist as it contains content "ignore-me" (Note: path pattern is still star-me)`, ({
+ expect,
+ }) => {
+ expect(fs.existsSync(path.resolve(exampleBuildDir, "client", "star-me", "ignore-me.js"))).toBe(
+ false,
+ );
+ });
+});
diff --git a/packages/esbuild-plugin-react18/__tests__/sourceReplacePatterns.test.ts b/packages/esbuild-plugin-react18/__tests__/sourceReplacePatterns.test.ts
new file mode 100644
index 00000000..43c2e9b4
--- /dev/null
+++ b/packages/esbuild-plugin-react18/__tests__/sourceReplacePatterns.test.ts
@@ -0,0 +1,53 @@
+import fs from "node:fs";
+import path from "node:path";
+import { describe, test, beforeAll } from "vitest";
+import esbuild from "esbuild";
+import react18Plugin from "../src";
+import glob from "tiny-glob";
+
+describe.concurrent("Test plugin with ignorePatterns -- without content pattern", async () => {
+ const outDir = "source-replace-patterns";
+ const exampleBuildDir = path.resolve(process.cwd(), "dist", outDir);
+ try {
+ fs.unlinkSync(path.resolve(exampleBuildDir));
+ } catch {}
+ beforeAll(async () => {
+ await esbuild.build({
+ format: "cjs",
+ target: "es2019",
+ sourcemap: false,
+ bundle: true,
+ minify: true,
+ plugins: [
+ react18Plugin({
+ sourceReplacePatterns: [
+ {
+ pathPattern: /constants/,
+ replaceParams: [
+ { pattern: /aaa/, substitute: "3c3c3c" },
+ { pattern: /#555/, substitute: "#ccc" },
+ ],
+ },
+ ],
+ }),
+ ],
+ entryPoints: await glob("../esbuild-plugin-react18-example/src/**/*.*"),
+ publicPath: "https://my.domain/static/",
+ external: ["react", "react-dom"],
+ outdir: "./dist/" + outDir,
+ });
+ });
+
+ test(`"use client"; directive should be present in client components`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "client", "index.js"), "utf-8");
+ expect(/^"use client";\n/m.test(text)).toBe(true);
+ });
+ test(`"use client"; directive should not be present in server components`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "server", "index.js"), "utf-8");
+ expect(/^"use client";\n/m.test(text)).toBe(false);
+ });
+ test(`defaultBgColor should be "#3c3c3c" and defaultColor should be "#ccc"`, ({ expect }) => {
+ const text = fs.readFileSync(path.resolve(exampleBuildDir, "server", "constants.js"), "utf-8");
+ expect(text.includes("3c3c3c")).toBe(true);
+ });
+});
diff --git a/packages/esbuild-plugin-react18/package.json b/packages/esbuild-plugin-react18/package.json
index f6663a75..98aaf136 100644
--- a/packages/esbuild-plugin-react18/package.json
+++ b/packages/esbuild-plugin-react18/package.json
@@ -2,7 +2,7 @@
"name": "esbuild-plugin-react18",
"author": "Mayank Kumar Chaudhari ",
"private": false,
- "version": "0.0.2",
+ "version": "0.0.3",
"description": "Unleash the Power of React Server Components! ESBuild plugin to build RSC (React18 Server Components) compatible libraries.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -16,6 +16,7 @@
"@vitest/coverage-v8": "^0.34.5",
"esbuild": "^0.18.17",
"octokit": "^3.1.0",
+ "tiny-glob": "^0.2.9",
"typescript": "^5.1.6",
"vitest": "^0.34.1"
},
diff --git a/packages/esbuild-plugin-react18/src/index.ts b/packages/esbuild-plugin-react18/src/index.ts
index 0a48c550..3966bd57 100644
--- a/packages/esbuild-plugin-react18/src/index.ts
+++ b/packages/esbuild-plugin-react18/src/index.ts
@@ -3,10 +3,10 @@ import fs from "node:fs";
import path from "node:path";
type React18PluginOptions = {
- /** do not ignore tese files */
+ /** to not ignore tese files */
keepTests?: boolean;
- /** do not remove `data-testid` attributes. If `keepTests` is true,
+ /** to not remove `data-testid` attributes. If `keepTests` is true,
* `data-testid` attributes will not be removed irrespective of
* `keepTestIds` value.
* This attribute is useful when setting `sourceReplacePatterns`
@@ -48,51 +48,53 @@ type React18PluginOptions = {
};
/** This plugin prevents building test files by esbuild. DTS may still geenrate type files for the tests with only { } as file content*/
-const react18Plugin: (options?: React18PluginOptions) => Plugin = options => ({
+const react18Plugin: (options: React18PluginOptions) => Plugin = options => ({
name: "esbuild-plugin-react18-" + uuid(),
setup(build) {
+ if (!options) options = {};
const ignoreNamespace = "mayank1513-ignore-" + uuid();
+ const keepNamespace = "mayank1513-keep-" + uuid();
const testPathRegExp = /.*\.(test|spec|check)\.(j|t)s(x)?$/i;
- if (!options?.keepTests) {
+
+ const write = build.initialOptions.write;
+ build.initialOptions.write = false;
+
+ if (!options.keepTests) {
build.onResolve({ filter: testPathRegExp }, args => ({
path: args.path,
namespace: ignoreNamespace,
}));
- if (!options?.keepTestIds) {
+ if (!options.keepTestIds) {
/** remove data-testid */
- build.onLoad({ filter: /.*\.(j|t)s(x)?$/, namespace: "file" }, args => {
- const text = fs.readFileSync(args.path, "utf8");
- const loader = args.path.slice(args.path.lastIndexOf(".") + 1);
- return {
- contents: text.replace(/\s*data-testid="[^"]*"/gm, " "),
- loader,
- } as OnLoadResult;
+ if (!options.sourceReplacePatterns) options.sourceReplacePatterns = [];
+ options.sourceReplacePatterns.push({
+ pathPattern: /.*\.(j|t)s(x)?$/,
+ replaceParams: [{ pattern: /\s*data-testid="[^"]*"/gm, substitute: " " }],
});
}
}
- options?.ignorePatterns?.forEach(ignorePattern => {
+ options.ignorePatterns?.forEach(ignorePattern => {
build.onResolve({ filter: ignorePattern.pathPattern }, args => {
/** remove content to avoid building/transpiling test files unnecessarily*/
- if (!ignorePattern.contentPatterns?.length)
- return {
- path: args.path,
- namespace: ignoreNamespace,
- };
- const text = fs.readFileSync(path.resolve(args.resolveDir, args.path), "utf8");
- for (const contentPattern of ignorePattern.contentPatterns) {
- if (contentPattern.test(text)) {
- return {
- path: args.path,
- namespace: ignoreNamespace,
- };
+ const fullPath = path.resolve(args.resolveDir, args.path);
+ if (!ignorePattern.contentPatterns?.length || !fs.existsSync(fullPath))
+ return { path: args.path, namespace: ignoreNamespace };
+
+ if (!fs.lstatSync(fullPath).isDirectory()) {
+ const text = fs.readFileSync(fullPath, "utf8");
+ for (const contentPattern of ignorePattern.contentPatterns) {
+ if (contentPattern.test(text)) {
+ return { path: args.path, namespace: ignoreNamespace };
+ }
}
}
- return { path: args.path };
+ return { path: fullPath, namespace: keepNamespace };
});
});
- options?.sourceReplacePatterns?.forEach(sourceReplacePattern => {
+ options.sourceReplacePatterns?.forEach(sourceReplacePattern => {
+ if (sourceReplacePattern.replaceParams.length === 0) return;
/** Add namespace file to avoid conflict with ignored files */
build.onLoad({ filter: sourceReplacePattern.pathPattern, namespace: "file" }, args => {
let contents = fs.readFileSync(args.path, "utf8");
@@ -109,10 +111,18 @@ const react18Plugin: (options?: React18PluginOptions) => Plugin = options => ({
build.onLoad({ filter: /.*/, namespace: ignoreNamespace }, args => {
/** remove content to avoid building/transpiling test files unnecessarily*/
- console.log("onLoad", args);
return { contents: "" };
});
+ build.onLoad({ filter: /.*/, namespace: keepNamespace }, args => {
+ if (fs.existsSync(args.path) && fs.lstatSync(args.path).isDirectory())
+ return { contents: "" };
+ else {
+ const loader = args.path.slice(args.path.lastIndexOf(".") + 1);
+ return { contents: fs.readFileSync(args.path, "utf-8"), loader } as OnLoadResult;
+ }
+ });
+
const useClientRegExp = /['"]use client['"]\s?;/i;
build.onEnd(result => {
@@ -121,14 +131,13 @@ const react18Plugin: (options?: React18PluginOptions) => Plugin = options => ({
.forEach(f => {
const txt = f.text;
if (txt.match(useClientRegExp)) {
- Object.defineProperty(f, "text", {
- value: '"use client";\n' + txt.replace(useClientRegExp, ""),
- });
+ const text = '"use client";\n' + txt.replace(useClientRegExp, "");
+ f.contents = new TextEncoder().encode(text);
}
});
/** handle buildReplacePatterns */
- options?.buildReplacePatterns?.forEach(buildReplacePattern => {
+ options.buildReplacePatterns?.forEach(buildReplacePattern => {
result.outputFiles
?.filter(f => buildReplacePattern.pathPattern.test(f.path))
.forEach(f => {
@@ -136,14 +145,24 @@ const react18Plugin: (options?: React18PluginOptions) => Plugin = options => ({
buildReplacePattern.replaceParams.forEach(({ pattern, substitute }) => {
text = text.replace(pattern, substitute);
});
- Object.defineProperty(f, "text", { value: text });
+ f.contents = new TextEncoder().encode(text);
});
});
/** Do not generate {empty} test files if keepTests is not set to true */
- if (!options?.keepTests) {
+ if (!options.keepTests) {
result.outputFiles = result.outputFiles?.filter(f => !testPathRegExp.test(f.path));
}
+
+ /** remove empty files */
+ result.outputFiles = result.outputFiles?.filter(f => f.text !== "");
+ /** assume true if undefined */
+ if (write === undefined || write) {
+ result.outputFiles?.forEach(file => {
+ fs.mkdirSync(path.dirname(file.path), { recursive: true });
+ fs.writeFileSync(file.path, file.contents);
+ });
+ }
});
},
});
diff --git a/packages/esbuild-plugin-react18/touchup.js b/packages/esbuild-plugin-react18/touchup.js
index 25a9c3e2..7b315ae8 100644
--- a/packages/esbuild-plugin-react18/touchup.js
+++ b/packages/esbuild-plugin-react18/touchup.js
@@ -10,16 +10,7 @@ delete packageJson.scripts;
packageJson.main = "index.js";
packageJson.types = "index.d.ts";
-fs.writeFileSync(
- path.resolve(__dirname, "dist", "package.json"),
- JSON.stringify(packageJson, null, 2),
-);
-
-fs.copyFileSync(
- path.resolve(__dirname, "..", "..", "README.md"),
- path.resolve(__dirname, "dist", "README.md"),
-);
-
+console.log(process.env.TOKEN, process.env.OWNER, process.env.REPO);
if (process.env.TOKEN) {
const { Octokit } = require("octokit");
// Octokit.js
@@ -55,3 +46,13 @@ if (process.env.TOKEN) {
console.log("octokit error", e);
}
}
+
+fs.writeFileSync(
+ path.resolve(__dirname, "dist", "package.json"),
+ JSON.stringify(packageJson, null, 2),
+);
+
+fs.copyFileSync(
+ path.resolve(__dirname, "..", "..", "README.md"),
+ path.resolve(__dirname, "dist", "README.md"),
+);