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: add a babel-plugin to add extensions to imports and handle aliases #576

Merged
merged 4 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/check-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ jobs:
- name: Typecheck
run: yarn typecheck

- name: Test
run: yarn test

- name: Build packages
run: yarn lerna run prepare
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"typecheck": "tsc --noEmit",
"watch": "concurrently 'yarn typecheck --watch' 'lerna run --parallel prepare -- --watch'",
"test": "yarn workspace react-native-builder-bob test",
"docs": "yarn workspace docs"
},
"devDependencies": {
Expand Down Expand Up @@ -48,7 +49,8 @@
"node_modules/",
"coverage/",
"lib/",
"templates/"
"templates/",
"__fixtures__/"
],
"prettier": {
"singleQuote": true,
Expand Down
12 changes: 10 additions & 2 deletions packages/react-native-builder-bob/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"prepare": "babel --extensions .ts,.tsx src --out-dir lib --source-maps --delete-dir-on-start"
"prepare": "babel --extensions .ts,.tsx src --out-dir lib --source-maps --delete-dir-on-start",
"test": "jest"
},
"jest": {
"testPathIgnorePatterns": [
"/lib/"
]
},
"dependencies": {
"@babel/core": "^7.18.5",
Expand All @@ -59,6 +65,7 @@
},
"devDependencies": {
"@babel/cli": "^7.17.10",
"@jest/globals": "^29.7.0",
"@types/babel__core": "^7.1.19",
"@types/browserslist": "^4.15.0",
"@types/cross-spawn": "^6.0.2",
Expand All @@ -70,6 +77,7 @@
"@types/prompts": "^2.0.14",
"@types/which": "^2.0.1",
"@types/yargs": "^17.0.10",
"concurrently": "^7.2.2"
"concurrently": "^7.2.2",
"jest": "^29.7.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "@";
import f from "@/f";
import "@something";
import "./@";
import "file";
import { something } from "something";
import "something/somefile";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "..";
import f from "../f";
import "@something";
import "./@";
import "../f";
import { something } from "another";
import "another/somefile";
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export * as a from "a";
export * as b from "./b";
export * as c from "./c";
export * as d from "./d";
export * as e from "./e.story";
export * as f from "../f";
export * as pac from "..";
export * as pak from "../";
export * as pax from "../index";

export { a as a1 } from "./a";
export * from "./b";

export type { A } from "./a";

export const foo = "foo";

const bar = "bar";

export { bar };
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export * as a from "a";
export * as b from "./b.mjs";
export * as c from "./c.mjs";
export * as d from "./d";
export * as e from "./e.story.mjs";
export * as f from "../f.mjs";
export * as pac from "../index.mjs";
export * as pak from "../index.mjs";
export * as pax from "../index.mjs";
export { a as a1 } from "./a.mjs";
export * from "./b.mjs";
export type { A } from "./a";
export const foo = "foo";
const bar = "bar";
export { bar };
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "./a";
import a from "a";
import b from "./b";
import c from "./c";
import d from "./d";
import e from "./e.story";
import f from "../f";
import pac from "..";
import pak from "../";
import pax from "../index";

import { a as a1 } from "./a";
import * as b1 from "./b";
import something, { c as c1 } from "./c";

import type { A } from "./a";
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "./a.mjs";
import a from "a";
import b from "./b.mjs";
import c from "./c.mjs";
import d from "./d";
import e from "./e.story.mjs";
import f from "../f.mjs";
import pac from "../index.mjs";
import pak from "../index.mjs";
import pax from "../index.mjs";
import { a as a1 } from "./a.mjs";
import * as b1 from "./b.mjs";
import something, { c as c1 } from "./c.mjs";
import type { A } from "./a";
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
60 changes: 60 additions & 0 deletions packages/react-native-builder-bob/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { expect, it } from '@jest/globals';
import { transformFileAsync } from '@babel/core';
import fs from 'node:fs';
import path from 'node:path';

it.each(['imports', 'exports'])(`adds .js extension to %s`, async (name) => {
const filepath = path.resolve(
__dirname,
`../__fixtures__/project/code/$${name}-input.ts`
);

const result = await transformFileAsync(filepath, {
configFile: false,
babelrc: false,
plugins: [
'@babel/plugin-syntax-typescript',
[require.resolve('../babel.ts'), { extension: 'mjs' }],
],
});

const expected = await fs.promises.readFile(
path.resolve(__dirname, `../__fixtures__/project/code/$${name}-output.ts`),
'utf8'
);

expect(result?.code).toEqual(expected.trim());
});

it('replaces alias imports', async () => {
const filepath = path.resolve(
__dirname,
`../__fixtures__/project/code/$alias-input.ts`
);

const result = await transformFileAsync(filepath, {
cwd: __dirname,
configFile: false,
babelrc: false,
plugins: [
'@babel/plugin-syntax-typescript',
[
require.resolve('../babel.ts'),
{
alias: {
'@': '../__fixtures__/project',
'file': '../__fixtures__/project/f',
'something': 'another',
},
},
],
],
});

const expected = await fs.promises.readFile(
path.resolve(__dirname, `../__fixtures__/project/code/$alias-output.ts`),
'utf8'
);

expect(result?.code).toEqual(expected.trim());
});
160 changes: 160 additions & 0 deletions packages/react-native-builder-bob/src/babel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import fs from 'fs';
import path from 'path';
import type { ConfigAPI, NodePath, PluginObj, PluginPass } from '@babel/core';
import type {
ImportDeclaration,
ExportAllDeclaration,
ExportNamedDeclaration,
} from '@babel/types';

type Options = {
alias?: Record<string, string>;
extension?: 'cjs' | 'mjs';
};

const isFile = (filename: string): boolean => {
const exists =
fs.lstatSync(filename, { throwIfNoEntry: false })?.isFile() ?? false;

return exists;
};

const isDirectory = (filename: string): boolean => {
const exists =
fs.lstatSync(filename, { throwIfNoEntry: false })?.isDirectory() ?? false;

return exists;
};

const isTypeImport = (
node: ImportDeclaration | ExportNamedDeclaration | ExportAllDeclaration
) =>
('importKind' in node && node.importKind === 'type') ||
('exportKind' in node && node.exportKind === 'type');

const assertFilename: (
filename: string | null | undefined
) => asserts filename is string = (filename) => {
if (filename == null) {
throw new Error("Couldn't find a filename for the current file.");
}
};

export default function (
api: ConfigAPI,
{ alias, extension }: Options
): PluginObj {
api.assertVersion(7);

function aliasImports(
{
node,
}: NodePath<
ImportDeclaration | ExportNamedDeclaration | ExportAllDeclaration
>,
state: PluginPass
) {
if (
alias == null ||
// Skip type imports as they'll be removed
isTypeImport(node) ||
// Skip imports without a source
!node.source?.value
) {
return;
}

assertFilename(state.filename);

const root = state.cwd;
const source = node.source.value;

for (const [key, value] of Object.entries(alias)) {
if (source === key || source.startsWith(`${key}/`)) {
const resolved = value.startsWith('.')
? path.relative(
path.dirname(state.filename),
path.resolve(root, value)
)
: value;

node.source.value = source.replace(key, resolved);
return;
}
}
}

function addExtension(
{
node,
}: NodePath<
ImportDeclaration | ExportNamedDeclaration | ExportAllDeclaration
>,
state: PluginPass
) {
if (
extension == null ||
// Skip type imports as they'll be removed
isTypeImport(node) ||
// Skip non-relative imports
!node.source?.value.startsWith('.')
) {
return;
}

assertFilename(state.filename);

// Skip folder imports
const filename = path.resolve(
path.dirname(state.filename),
node.source.value
);

// Add extension if .ts file or file with extension exists
if (
isFile(`${filename}.ts`) ||
isFile(`${filename}.tsx`) ||
isFile(`${filename}.${extension}`)
) {
node.source.value += `.${extension}`;
return;
}

// Replace .ts extension with .js if .ts file exists
if (isFile(filename)) {
node.source.value = node.source.value.replace(/\.tsx?$/, `.${extension}`);
return;
}

if (
isDirectory(filename) &&
(isFile(path.join(filename, 'index.ts')) ||
isFile(path.join(filename, 'index.tsx')) ||
isFile(path.join(filename, `index.${extension}`)))
) {
node.source.value = node.source.value.replace(
/\/?$/,
`/index.${extension}`
);
return;
}
}

return {
name: '@builder-bob/babel-plugin',
visitor: {
ImportDeclaration(path, state) {
aliasImports(path, state);
addExtension(path, state);
},
ExportNamedDeclaration(path, state) {
aliasImports(path, state);
addExtension(path, state);
},
ExportAllDeclaration(path, state) {
aliasImports(path, state);
addExtension(path, state);
},
},
};
}
6 changes: 5 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@
"target": "esnext",
"verbatimModuleSyntax": true
},
"exclude": ["lib", "packages/create-react-native-library/templates"]
"exclude": [
"lib",
"packages/react-native-builder-bob/src/__fixtures__",
"packages/create-react-native-library/templates"
]
}
Loading
Loading