Skip to content

Commit

Permalink
feat: use plugin system instead of inject option to manage jsx-runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
adbayb committed Apr 7, 2021
1 parent be667a3 commit 0718521
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 52 deletions.
4 changes: 4 additions & 0 deletions examples/ts-module/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"devDependencies": {
"@types/react": "17.0.3",
"@types/react-dom": "17.0.3",
"preact": "10.5.13",
"quickbundle": "0.1.1",
"typescript": "4.2.3"
},
"peerDependencies": {
"preact": ">=10"
}
}
7 changes: 7 additions & 0 deletions examples/ts-module/src/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const Button = () => {
return (
<>
<button>Click me!</button>Plop
</>
);
};
2 changes: 2 additions & 0 deletions examples/ts-module/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { Button } from "./Button";

export const version = "0.0.0";

export const getCWD = () => {
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-preact/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
</head>
<body>
<div id="root" />
<script src="../dist/example.esm.js"></script>
<script src="../dist/index.mjs"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion examples/ts-react/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
</head>
<body>
<div id="root" />
<script src="../dist/example.esm.js"></script>
<script src="../dist/index.mjs"></script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/quickbundle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"prepare": "yarn build",
"prestart": "yarn build",
"start": "bin/index.js",
"clean": "rm -rf bin",
"build": "tsc && chmod +x bin/index.js",
"watch": "tsc -w"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/quickbundle/public/buildPresets/preact.js

This file was deleted.

3 changes: 0 additions & 3 deletions packages/quickbundle/public/buildPresets/react.js

This file was deleted.

130 changes: 86 additions & 44 deletions packages/quickbundle/src/commands/build.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,27 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { resolve } from "path";
import { BuildOptions, build } from "esbuild";
import { build } from "esbuild";
import { run } from "@adbayb/terminal-kit";
import { CWD } from "../constants";

// @todo: invariant/assert checks (if no source field is provided in package.json => error)
// @todo: clean before building (share clean method with the clean command)
// @todo: support externals
// @todo: run tsc to emit declaration file based upon pkgMetadata target

type BundleFormat = "esm" | "cjs";

type PackageMetadata = {
main: string;
module: string;
source: string;
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
};

type Project = ReturnType<typeof createProject>;

const createProject = () => {
const {
dependencies = {},
devDependencies = {},
peerDependencies = {},
main,
module,
source,
}: PackageMetadata = require(resolve(CWD, "package.json"));
const allDependencies = [
...Object.keys(dependencies),
...Object.keys(devDependencies),
...Object.keys(peerDependencies),
];

const { main, module, source }: PackageMetadata = require(resolve(
CWD,
"package.json"
));
// @todo: invariant/asserts for main/module/source

return {
Expand All @@ -45,43 +30,56 @@ const createProject = () => {
cjs: main,
esm: module,
},
hasModule(name: string) {
return allDependencies.includes(name);
},
} as const;
};

const getInjectPresets = (project: Project): BuildOptions["inject"] => {
const availablePresets = ["preact", "react"]; // @note: the order is important (search first preact before react)

for (const preset of availablePresets) {
if (project.hasModule(preset)) {
return [
resolve(__dirname, `../../public/buildPresets/${preset}.js`),
];
}
const hasModule = (name: string) => {
try {
return Boolean(require.resolve(name));
} catch (error) {
return false;
}
};

return;
type TypeScriptConfiguration = {
target: string | undefined;
hasJsxRuntime: boolean;
};

const createBundler = async (project: Project) => {
const injectPresets = getInjectPresets(project);
const getTypeScriptOptions = async (): Promise<TypeScriptConfiguration> => {
const ts = await import("typescript"); // @note: lazy load typescript only if necessary
const tsMetadata = ts.parseJsonConfigFileContent(
const { jsx, target } = ts.parseJsonConfigFileContent(
require(resolve(CWD, "tsconfig.json")),
ts.sys,
CWD
).options;

// @todo: prevent issues if no typescript or tsconfig provided
const tsTarget = tsMetadata.target || ts.ScriptTarget.ESNext;
// @note: convert ts target value to esbuild ones (latest value is not supported)
const target = [ts.ScriptTarget.ESNext, ts.ScriptTarget.Latest].includes(
tsTarget
)
? "esnext"
: ts.ScriptTarget[tsTarget]?.toLowerCase();
const esbuildTarget =
!target ||
[ts.ScriptTarget.ESNext, ts.ScriptTarget.Latest].includes(target)
? "esnext"
: ts.ScriptTarget[target]?.toLowerCase();

return {
target: esbuildTarget,
hasJsxRuntime:
jsx !== undefined &&
[ts.JsxEmit["ReactJSX"], ts.JsxEmit["ReactJSXDev"]].includes(jsx),
};
};

const createBundler = async (project: Project) => {
let target: string | undefined;
const isTypeScriptProject = hasModule("typescript");
let tsOptions: TypeScriptConfiguration | undefined;

if (isTypeScriptProject) {
tsOptions = await getTypeScriptOptions();

target = tsOptions.target;
}

return (format: BundleFormat, isProduction?: boolean) => {
return build({
Expand All @@ -95,11 +93,54 @@ const createBundler = async (project: Project) => {
entryPoints: [project.source],
outfile: project.destination[format],
tsconfig: "tsconfig.json",
target,
target: target || "esnext",
format,
minify: isProduction,
sourcemap: !isProduction,
inject: injectPresets,
plugins: [
{
// @note: Plugin to automatically inject React import for jsx management
// ESBuild doesn't support `jsx` tsconfig field: this plugin aims to add a tiny wrapper to support it
// We could use the `inject` ESBuild feature but it will break the tree shaking behavior since the React import will
// be imported on each file (even in .ts file) leading React being included in the bundle even if not needed
name: "jsx-runtime",
setup(build) {
const fs = require("fs");
// @todo answer https://github.com/evanw/esbuild/issues/334

build.onLoad(
{ filter: /\.(j|t)sx$/ },
async ({ path }) => {
const module = ["preact", "react"].find(
hasModule
);

// @note: enable plugin only if
// - `${module}/jsx-runtime` package is available (for js project, it's the only condition to check!)
// - if ts project: jsx compilerOption === "react-jsx" or "react-jsxdev"
if (
!module ||
!hasModule(`${module}/jsx-runtime`) ||
(isTypeScriptProject &&
!tsOptions?.hasJsxRuntime)
) {
return;
}

const content: string = await fs.promises.readFile(
path,
"utf8"
);

return {
contents: `import * as React from "${module}";${content}`,
loader: "jsx",
};
}
);
},
},
],
});
};
};
Expand All @@ -110,6 +151,7 @@ const main = async () => {
const formats: BundleFormat[] = ["cjs", "esm"];

for (const format of formats) {
// @todo: isProduction true for build command and false for watch command:
await run(`Building ${format} 👷‍♂️`, bundle(format, false));
}
};
Expand Down

0 comments on commit 0718521

Please sign in to comment.