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 ability to use tsconfig paths aliases other than ~ #2412

Merged
merged 12 commits into from
Mar 31, 2022
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
- eps1lon
- evanwinter
- exegeteio
- F3n67u
- fergusmeiklejohn
- fgiuliani
- fishel-feng
Expand Down
5 changes: 4 additions & 1 deletion integration/helpers/create-fixture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import express from "express";
import cheerio from "cheerio";
import prettier from "prettier";
import getPort from "get-port";
import stripIndent from "strip-indent";

import type {
ServerBuild,
Expand Down Expand Up @@ -43,6 +44,8 @@ export type Fixture = Awaited<ReturnType<typeof createFixture>>;
export type AppFixture = Awaited<ReturnType<typeof createAppFixture>>;

export const js = String.raw;
export const json = String.raw;
export const mdx = String.raw;

export async function createFixture(init: FixtureInit) {
let projectDir = await createFixtureProject(init);
Expand Down Expand Up @@ -432,7 +435,7 @@ async function writeTestFiles(init: FixtureInit, dir: string) {
Object.keys(init.files).map(async (filename) => {
let filePath = path.join(dir, filename);
await fse.ensureDir(path.dirname(filePath));
await fs.writeFile(filePath, init.files[filename]);
await fs.writeFile(filePath, stripIndent(init.files[filename]));
})
);
await renamePkgJsonApp(dir);
Expand Down
126 changes: 126 additions & 0 deletions integration/path-mapping-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
createAppFixture,
createFixture,
js,
json,
mdx,
} from "./helpers/create-fixture";
import type { Fixture, AppFixture } from "./helpers/create-fixture";

let fixture: Fixture;
let app: AppFixture;

beforeAll(async () => {
fixture = await createFixture({
files: {
"app/components/my-lib/index.ts": js`
export const pizza = "this is a pizza";
`,

"app/routes/index.tsx": js`
import { pizza } from "@mylib";
import { json, useLoaderData, Link } from "remix";

export function loader() {
return json(pizza);
}

export default function Index() {
let data = useLoaderData();
return (
<div>
{data}
</div>
)
}
`,

"app/routes/tilde-alias.tsx": js`
import { pizza } from "~/components/my-lib";
import { json, useLoaderData, Link } from "remix";

export function loader() {
return json(pizza);
}

export default function Index() {
let data = useLoaderData();
return (
<div>
{data}
</div>
)
}
`,

"app/components/component.jsx": js`
export function PizzaComponent() {
return <span>this is a pizza</span>
}
`,

"app/routes/mdx.mdx": mdx`
---
meta:
title: My First Post
description: Isn't this awesome?
headers:
Cache-Control: no-cache
---

import { PizzaComponent } from "@component";

# Hello MDX!

This is my first post.

<PizzaComponent />
`,

"tsconfig.json": json`
{
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"resolveJsonModule": true,
"target": "ES2019",
"strict": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"],
"@mylib": ["./app/components/my-lib/index"],
"@component": ["./app/components/component.jsx"],
},

// Remix takes care of building everything in \`remix build\`.
"noEmit": true
}
}
`,
},
});

app = await createAppFixture(fixture);
});

afterAll(async () => app.close());

it("import internal library via alias other than ~", async () => {
// test for https://github.com/remix-run/remix/issues/2298
let response = await fixture.requestDocument("/");
expect(await response.text()).toMatch("this is a pizza");
});

it("import internal library via ~ alias", async () => {
let response = await fixture.requestDocument("/tilde-alias");
expect(await response.text()).toMatch("this is a pizza");
});

it("works for mdx files", async () => {
let response = await fixture.requestDocument("/mdx");
expect(await response.text()).toMatch("this is a pizza");
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"simple-git": "^3.2.4",
"sort-package-json": "^1.54.0",
"strip-ansi": "^6.0.1",
"strip-indent": "^3.0.0",
"type-fest": "^2.11.1",
"typescript": "^4.5.5",
"unified": "^9.2.0"
Expand Down
32 changes: 26 additions & 6 deletions packages/remix-dev/compiler/plugins/mdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { remarkMdxFrontmatter } from "remark-mdx-frontmatter";

import type { RemixConfig } from "../../config";
import { getLoaderForFile } from "../loaders";
import { createMatchPath } from "../utils/tsconfig";

export function mdxPlugin(config: RemixConfig): esbuild.Plugin {
return {
Expand All @@ -16,17 +17,36 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin {
]);

build.onResolve({ filter: /\.mdx?$/ }, (args) => {
let matchPath = createMatchPath();
// Resolve paths according to tsconfig paths property
function resolvePath(id: string) {
if (!matchPath) {
return id;
}
return (
matchPath(id, undefined, undefined, [
".ts",
".tsx",
".js",
".jsx",
".mdx",
".md",
]) || id
);
}

let resolvedPath = resolvePath(args.path);
let resolved = path.resolve(args.resolveDir, resolvedPath);

return {
path: args.path.startsWith("~/")
? path.resolve(config.appDirectory, args.path.replace(/^~\//, ""))
: path.resolve(args.resolveDir, args.path),
path: resolved,
namespace: "mdx",
};
});

build.onLoad({ filter: /\.mdx?$/ }, async (args) => {
try {
let contents = await fsp.readFile(args.path, "utf-8");
let fileContents = await fsp.readFile(args.path, "utf-8");

let rehypePlugins = [];
let remarkPlugins = [
Expand Down Expand Up @@ -54,7 +74,7 @@ export const meta = typeof attributes !== "undefined" && attributes.meta;
export const links = undefined;
`;

let compiled = await xdm.compile(contents, {
let compiled = await xdm.compile(fileContents, {
jsx: true,
jsxRuntime: "classic",
pragma: "React.createElement",
Expand All @@ -63,7 +83,7 @@ export const links = undefined;
remarkPlugins,
});

contents = `
let contents = `
${compiled.value}
${remixExports}`;

Expand Down
21 changes: 14 additions & 7 deletions packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
serverBuildVirtualModule,
assetsManifestVirtualModule,
} from "../virtualModules";
import { createMatchPath } from "../utils/tsconfig";

/**
* A plugin responsible for resolving bare module ids based on server target.
Expand All @@ -19,12 +20,23 @@ export function serverBareModulesPlugin(
dependencies: Record<string, string>,
onWarning?: (warning: string, key: string) => void
): Plugin {
let matchPath = createMatchPath();
// Resolve paths according to tsconfig paths property
function resolvePath(id: string) {
MichaelDeBoey marked this conversation as resolved.
Show resolved Hide resolved
if (!matchPath) {
return id;
}
return (
matchPath(id, undefined, undefined, [".ts", ".tsx", ".js", ".jsx"]) || id
);
}

return {
name: "server-bare-modules",
setup(build) {
build.onResolve({ filter: /.*/ }, ({ importer, path }) => {
// If it's not a bare module ID, bundle it.
if (!isBareModuleId(path)) {
if (!isBareModuleId(resolvePath(path))) {
return undefined;
}

Expand Down Expand Up @@ -114,12 +126,7 @@ function getNpmPackageName(id: string): string {
}

function isBareModuleId(id: string): boolean {
return (
!id.startsWith("node:") &&
!id.startsWith(".") &&
!id.startsWith("~") &&
!isAbsolute(id)
);
return !id.startsWith("node:") && !id.startsWith(".") && !isAbsolute(id);
}

function warnOnceIfEsmOnlyPackage(
Expand Down
65 changes: 65 additions & 0 deletions packages/remix-dev/compiler/utils/tsconfig/configLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as path from "path";

import { tsConfigLoader } from "./tsConfigLoader";

export interface ConfigLoaderParams {
cwd: string;
}

export interface ConfigLoaderSuccessResult {
resultType: "success";
configFileAbsolutePath: string;
baseUrl: string;
absoluteBaseUrl: string;
paths: { [key: string]: Array<string> };
mainFields?: Array<string>;
addMatchAll?: boolean;
}

export interface ConfigLoaderFailResult {
resultType: "failed";
message: string;
}

export type ConfigLoaderResult =
| ConfigLoaderSuccessResult
| ConfigLoaderFailResult;

export function loadTsConfig(cwd: string = process.cwd()): ConfigLoaderResult {
return configLoader({ cwd: cwd });
}

export function configLoader({
cwd,
}: ConfigLoaderParams): ConfigLoaderResult {
// Load tsconfig and create path matching function
let loadResult = tsConfigLoader({
cwd,
getEnv: (key: string) => process.env[key],
});

if (!loadResult.tsConfigPath) {
return {
resultType: "failed",
message: "Couldn't find tsconfig.json",
};
}

if (!loadResult.baseUrl) {
return {
resultType: "failed",
message: "Missing baseUrl in compilerOptions",
};
}

let tsConfigDir = path.dirname(loadResult.tsConfigPath);
let absoluteBaseUrl = path.join(tsConfigDir, loadResult.baseUrl);

return {
resultType: "success",
configFileAbsolutePath: loadResult.tsConfigPath,
baseUrl: loadResult.baseUrl,
absoluteBaseUrl,
paths: loadResult.paths || {},
};
}
20 changes: 20 additions & 0 deletions packages/remix-dev/compiler/utils/tsconfig/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import tsConfigPaths from "tsconfig-paths";

import { loadTsConfig } from "./configLoader";
export { loadTsConfig } from "./configLoader";

export function createMatchPath() {
let configLoaderResult = loadTsConfig();
if (configLoaderResult.resultType === "failed") {
return undefined;
}

let matchPath = tsConfigPaths.createMatchPath(
configLoaderResult.absoluteBaseUrl,
configLoaderResult.paths,
configLoaderResult.mainFields,
configLoaderResult.addMatchAll
);

return matchPath;
}