Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(remix-dev): add jsxImportSource config option #2216

Closed
wants to merge 11 commits into from
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
- johannesbraeunig
- johnson444
- johnson444
- jolle
- joms
- joshball
- jssisodiya
Expand Down
310 changes: 137 additions & 173 deletions packages/remix-dev/__tests__/build-test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,102 @@
import path from "path";
import type { RollupOutput } from "rollup";
import type * as esbuild from "esbuild";

import { BuildMode, BuildTarget } from "../build";
import type { BuildOptions } from "../compiler";
import { build, generate } from "../compiler";
import type { BuildOptions } from "../build";
import { build } from "../compiler";
import type { RemixConfig } from "../config";
import { readConfig } from "../config";

const remixRoot = path.resolve(__dirname, "../../../fixtures/gists-app");
// Mock ESM-only modules as these are not well-supported in
// the Jest environment available.
jest.mock("xdm", () => ({
__esModule: true,
compile: () => ({}),
}));
jest.mock("remark-frontmatter", () => ({
__esModule: true,
}));
jest.mock("tsconfig-paths", () => ({
__esModule: true,
default: {
loadConfig() {
return {
resultType: "failed",
};
},
},
}));
Comment on lines +10 to +28
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not optimal but regardless of what I did to Babel or Jest these modules seemed to break Jest one way or another.


// a simple app that does not use ESM
const remixRoot = path.resolve(__dirname, "./fixtures/simple-app");
// a simple app like above, expect has a custom "jsxImportSource"
const customJSXAppRoot = path.resolve(__dirname, "./fixtures/custom-jsx-app");

const expectedBuildFiles = [
"public/build/entry.client-*.js",
"public/build/root-*.js",
"public/build/routes/index-*.js",
"public/build/_shared/chunk-*.js",
"build/index.js",
];

async function generateBuild(config: RemixConfig, options: BuildOptions) {
return await generate(await build(config, options));
return await build(config, { ...options, write: false });
}

function getFilenames(output: (esbuild.BuildResult | undefined)[]) {
return output.flatMap(({ outputFiles }) =>
outputFiles.map(({ path: outputPath }) =>
path.relative(remixRoot, outputPath)
)
);
}

function getFilenames(output: RollupOutput) {
return output.output.map((item) => item.fileName).sort();
/**
* Checks that the esbuild output has the specified files, with
* "*" acting as a wildcard, e.g., for hashes.
*/
function expectBuildToHaveFiles(
output: (esbuild.BuildResult | undefined)[],
files: string[]
) {
expect(getFilenames(output)).toEqual(
expect.arrayContaining(
files.map((string) =>
expect.stringMatching(
new RegExp(
`^${string.replace(/\//g, "\\/").replace(/\*/g, "[^/]+")}$`
)
)
)
)
);
}

describe.skip("building", () => {
// describe("building", () => {
/**
* Runs the build output with mocked require-resolving and returns
* the mocked "module" object.
*/
function runBuildOutput(
text: string,
modules: Record<string, unknown> = {},
additionalArguments: Record<string, unknown> = {}
) {
let module = { exports: {} } as {
exports: { [key: string]: unknown };
};

// eslint-disable-next-line no-new-func
new Function("require", "module", ...Object.keys(additionalArguments), text)(
(moduleName: string) => modules[moduleName] ?? {},
module,
...Object.values(additionalArguments)
);

return { module };
}

describe("building", () => {
let config: RemixConfig;
beforeAll(async () => {
config = await readConfig(remixRoot);
Expand All @@ -28,191 +106,77 @@ describe.skip("building", () => {
jest.setTimeout(20000);
});

describe("the development server build", () => {
describe("the development build", () => {
it("generates the correct bundles", async () => {
let output = await generateBuild(config, {
mode: BuildMode.Development,
target: BuildTarget.Server,
target: BuildTarget.Node14,
});

expect(getFilenames(output)).toMatchInlineSnapshot(`
Array [
"_shared/Shared-072c977d.js",
"_shared/_rollupPluginBabelHelpers-8a275fd9.js",
"entry.server.js",
"index.js",
"pages/one.js",
"pages/two.js",
"root.js",
"routes/404.js",
"routes/gists.js",
"routes/gists.mine.js",
"routes/gists/$username.js",
"routes/gists/index.js",
"routes/index.js",
"routes/links.js",
"routes/loader-errors.js",
"routes/loader-errors/nested.js",
"routes/methods.js",
"routes/page/four.js",
"routes/page/three.js",
"routes/prefs.js",
"routes/render-errors.js",
"routes/render-errors/nested.js",
]
`);
expectBuildToHaveFiles(output, expectedBuildFiles);
});
});

describe("the production server build", () => {
it("generates the correct bundles", async () => {
let output = await generateBuild(config, {
mode: BuildMode.Production,
target: BuildTarget.Server,
});
it('calls the JSX custom factory with a custom "jsxImportSource"', async () => {
let buildOutput = await generateBuild(
await readConfig(customJSXAppRoot),
{
mode: BuildMode.Development,
}
);

expect(getFilenames(output)).toMatchInlineSnapshot(`
Array [
"_shared/Shared-072c977d.js",
"_shared/_rollupPluginBabelHelpers-8a275fd9.js",
"entry.server.js",
"index.js",
"pages/one.js",
"pages/two.js",
"root.js",
"routes/404.js",
"routes/gists.js",
"routes/gists.mine.js",
"routes/gists/$username.js",
"routes/gists/index.js",
"routes/index.js",
"routes/links.js",
"routes/loader-errors.js",
"routes/loader-errors/nested.js",
"routes/methods.js",
"routes/page/four.js",
"routes/page/three.js",
"routes/prefs.js",
"routes/render-errors.js",
"routes/render-errors/nested.js",
]
`);
let jsxFn = jest.fn();
let { module } = runBuildOutput(
buildOutput[1].outputFiles[0].text,
{},
{ jestFn: jsxFn }
);

expect(module.exports).toHaveProperty(
"routes.routes/index.module.default"
);

let indexRouteModule =
module.exports.routes["routes/index"].module.default;
expect(typeof indexRouteModule).toBe("function");

indexRouteModule();

expect(jsxFn).toBeCalledWith("div", null);
});
});

describe("the development browser build", () => {
it("generates the correct bundles", async () => {
let output = await generateBuild(config, {
it('calls the default React factory without a "jsxImportSource"', async () => {
let buildOutput = await generateBuild(config, {
mode: BuildMode.Development,
target: BuildTarget.Browser,
});

expect(getFilenames(output)).toMatchInlineSnapshot(`
Array [
"_shared/Shared-7d084ccf.js",
"_shared/__babel/runtime-88c72f87.js",
"_shared/__mdx-js/react-4b004046.js",
"_shared/__remix-run/react-cf018015.js",
"_shared/_rollupPluginBabelHelpers-bfa6c712.js",
"_shared/history-7c196d23.js",
"_shared/object-assign-510802f4.js",
"_shared/prop-types-1122a697.js",
"_shared/react-a3c235ca.js",
"_shared/react-dom-ec89bb6e.js",
"_shared/react-is-6b44b080.js",
"_shared/react-router-dom-ef82d700.js",
"_shared/react-router-e7697632.js",
"_shared/scheduler-8fd1645e.js",
"components/guitar-1080x720-a9c95518.jpg",
"components/guitar-2048x1365-f42efd6b.jpg",
"components/guitar-500x333-3a1a0bd1.jpg",
"components/guitar-500x500-c6f1ab94.jpg",
"components/guitar-600x600-b329e428.jpg",
"components/guitar-720x480-729becce.jpg",
"entry.client.js",
"manifest-8c53378e.js",
"pages/one.js",
"pages/two.js",
"root.js",
"routes/404.js",
"routes/gists.js",
"routes/gists.mine.js",
"routes/gists/$username.js",
"routes/gists/index.js",
"routes/index.js",
"routes/links.js",
"routes/loader-errors.js",
"routes/loader-errors/nested.js",
"routes/methods.js",
"routes/page/four.js",
"routes/page/three.js",
"routes/prefs.js",
"routes/render-errors.js",
"routes/render-errors/nested.js",
"styles/app-72f634dc.css",
"styles/gists-d7ad5f49.css",
"styles/methods-d182a270.css",
"styles/redText-2b391c21.css",
]
`);
let jsxFn = jest.fn();
let { module } = runBuildOutput(buildOutput[1].outputFiles[0].text, {
react: {
createElement: jsxFn,
},
});

expect(module.exports).toHaveProperty(
"routes.routes/index.module.default"
);

let indexRouteModule =
module.exports.routes["routes/index"].module.default;
expect(typeof indexRouteModule).toBe("function");

indexRouteModule();

expect(jsxFn).toBeCalledWith("div", null);
});
});

describe("the production browser build", () => {
describe("the production build", () => {
it("generates the correct bundles", async () => {
let output = await generateBuild(config, {
mode: BuildMode.Production,
target: BuildTarget.Browser,
});

expect(getFilenames(output)).toMatchInlineSnapshot(`
Array [
"_shared/Shared-bae6070c.js",
"_shared/__babel/runtime-88c72f87.js",
"_shared/__mdx-js/react-a9edf40b.js",
"_shared/__remix-run/react-991ebd19.js",
"_shared/_rollupPluginBabelHelpers-bfa6c712.js",
"_shared/history-e6417d88.js",
"_shared/object-assign-510802f4.js",
"_shared/prop-types-939a16b3.js",
"_shared/react-dom-9dcf9947.js",
"_shared/react-e3656f88.js",
"_shared/react-is-5765fb91.js",
"_shared/react-router-dom-baf54395.js",
"_shared/react-router-fc62a14c.js",
"_shared/scheduler-f1282356.js",
"components/guitar-1080x720-a9c95518.jpg",
"components/guitar-2048x1365-f42efd6b.jpg",
"components/guitar-500x333-3a1a0bd1.jpg",
"components/guitar-500x500-c6f1ab94.jpg",
"components/guitar-600x600-b329e428.jpg",
"components/guitar-720x480-729becce.jpg",
"entry.client-b7de4be6.js",
"manifest-943fff78.js",
"pages/one-829d2fc6.js",
"pages/two-31b88726.js",
"root-de6ed2a5.js",
"routes/404-a4edec5f.js",
"routes/gists-236207fe.js",
"routes/gists.mine-ac017552.js",
"routes/gists/$username-c4819bb8.js",
"routes/gists/index-0f39313f.js",
"routes/index-eb238abf.js",
"routes/links-50cd630a.js",
"routes/loader-errors-e4502176.js",
"routes/loader-errors/nested-741a07ef.js",
"routes/methods-8241c6fa.js",
"routes/page/four-efa66f69.js",
"routes/page/three-dfbf7520.js",
"routes/prefs-12bae83f.js",
"routes/render-errors-cb72f859.js",
"routes/render-errors/nested-ef1c619f.js",
"styles/app-72f634dc.css",
"styles/gists-d7ad5f49.css",
"styles/methods-d182a270.css",
"styles/redText-2b391c21.css",
]
`);
expectBuildToHaveFiles(output, expectedBuildFiles);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
/public/build
.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { RemixBrowser } from "@remix-run/react";
import { hydrate } from "react-dom";

hydrate(<RemixBrowser />, document);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
let markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);

responseHeaders.set("Content-Type", "text/html");

return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}