Skip to content

Commit

Permalink
Merge pull request #1377 from okta/jb-import-specifiers-2
Browse files Browse the repository at this point in the history
build fully specified imports for prod
  • Loading branch information
Jeff Belser committed Mar 17, 2022
2 parents 153cf41 + a6a0abf commit 8b6493d
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/babel-plugin-fully-specified/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"env": {
"test": {
"presets": ["@babel/preset-env"]
}
}
}
28 changes: 28 additions & 0 deletions packages/babel-plugin-fully-specified/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@okta/babel-plugin-fully-specified",
"version": "0.0.0",
"private": true,
"description": "Babel plugin to generate fully specified ESM module syntax",
"author": "Okta, Inc.",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/okta/odyssey",
"directory": "packages/babel-plugin-fully-specified"
},
"scripts": {
"prepare": "tsc",
"test": "jest",
"prepack": "prepack"
},
"dependencies": {
"@babel/types": "^7.14.5"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"babel-jest": "^26.6.3",
"jest": "^26.6.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`plugin should fully qualify the module exports specifiers 1`] = `
"export { level } from \\"./index.js\\";
export { levelIndex } from \\"./index.js\\";
export { oneBackLevel } from \\"../index.js\\";
export { oneBackLevelIndex } from \\"../index.js\\";
export { twoBackLevel } from \\"../../index.js\\";
export { twoBackLevelIndex } from \\"../../index.js\\";
export { somethingBack } from \\"../lib/something.js\\";
export { somethingBackTest } from \\"../lib/something.test.js\\";
export { something } from \\"./lib/something.js\\";
export { somethingTest } from \\"./lib/something.test.js\\";
export { something as another } from \\"./lib/something.js\\";
export { somethingTest as anotherTest } from \\"./lib/something.test.js\\";
export * as anotherModule from \\"./lib/something.js\\";
export * as anotherModuleTest from \\"./lib/something.test.js\\";
export * from \\"./lib/something.js\\";
export * from \\"./lib/something.test.js\\";"
`;

exports[`plugin should fully qualify the module imports specifiers 1`] = `
"import { level } from \\"./index.js\\";
import { levelIndex } from \\"./index.js\\";
import { oneBackLevel } from \\"../index.js\\";
import { oneBackLevelIndex } from \\"../index.js\\";
import { twoBackLevel } from \\"../../index.js\\";
import { twoBackLevelIndex } from \\"../../index.js\\";
import { somethingBack } from \\"../lib/something.js\\";
import { somethingBackTest } from \\"../lib/something.test.js\\";
import { export1, export2 as alias2 } from \\"./lib/something.js\\";
import { export1Test, export2 as alias2Test } from \\"./lib/something.test.js\\";
import { something } from \\"./lib/something.js\\";
import { somethingTest } from \\"./lib/something.test.js\\";
import { something as other } from \\"./lib/something.js\\";
import { something as otherTest } from \\"./lib/something.test.js\\";
import anotherImport from \\"./lib/something.js\\";
import anotherImportTest from \\"./lib/something.test.js\\";
import another, { otherImport } from \\"./lib/something.js\\";
import anotherTest, { otherImportTest } from \\"./lib/something.test.js\\";
import * as Something from \\"./lib/something.js\\";
import * as SomethingTest from \\"./lib/something.test.js\\";"
`;

exports[`plugin should skip bare module specifiers 1`] = `
"import babel from '@babel/core';
import { transform } from '@babel/core';
export * from '@babel/core';
export { transform } from '@babel/core';"
`;

exports[`plugin should skip style module imports 1`] = `
"import stylesWithMultipleExtensions from './styles.module.scss';
import styles from './styles.scss';"
`;

exports[`plugin should skip type-only exports 1`] = `
"export type { NamedType } from './lib/something';
export type { NamedTypeTest } from './lib/something.test';"
`;

exports[`plugin should skip type-only imports 1`] = `
"import type DefaultType from './lib/something';
import type DefaultTypeTest from './lib/something.test';
import type { NamedType } from './lib/something';
import type { NamedTypeTest } from './lib/something.test';
import type * as AllTypes from './lib/something';
import type * as AllTypesTest from './lib/something.test';"
`;
115 changes: 115 additions & 0 deletions packages/babel-plugin-fully-specified/src/__tests__/plugin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*!
* Copyright (c) 2021-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import { transformSync } from "@babel/core";
import plugin from "../../dist";

const importStatements = `
import { level } from '.'
import { levelIndex } from './'
import { oneBackLevel } from '..'
import { oneBackLevelIndex } from '../'
import { twoBackLevel } from '../..'
import { twoBackLevelIndex } from '../../'
import { somethingBack } from '../lib/something'
import { somethingBackTest } from '../lib/something.test'
import { export1 , export2 as alias2 } from './lib/something'
import { export1Test , export2 as alias2Test } from './lib/something.test'
import { something } from './lib/something'
import { somethingTest } from './lib/something.test'
import { something as other } from './lib/something'
import { something as otherTest } from './lib/something.test'
import anotherImport from './lib/something'
import anotherImportTest from './lib/something.test'
import another, { otherImport } from './lib/something'
import anotherTest, { otherImportTest } from './lib/something.test'
import * as Something from './lib/something'
import * as SomethingTest from './lib/something.test'
`;

const exportStatements = `
export { level } from '.'
export { levelIndex } from './'
export { oneBackLevel } from '..'
export { oneBackLevelIndex } from '../'
export { twoBackLevel } from '../..'
export { twoBackLevelIndex } from '../../'
export { somethingBack } from '../lib/something'
export { somethingBackTest } from '../lib/something.test'
export { something } from './lib/something'
export { somethingTest } from './lib/something.test'
export { something as another } from './lib/something'
export { somethingTest as anotherTest } from './lib/something.test'
export * as anotherModule from './lib/something'
export * as anotherModuleTest from './lib/something.test'
export * from './lib/something'
export * from './lib/something.test'
`;

const typeOnlyExports = `
export type { NamedType } from './lib/something'
export type { NamedTypeTest } from './lib/something.test'
`;

const typeOnlyImports = `
import type DefaultType from './lib/something'
import type DefaultTypeTest from './lib/something.test'
import type { NamedType } from './lib/something'
import type { NamedTypeTest } from './lib/something.test'
import type * as AllTypes from './lib/something'
import type * as AllTypesTest from './lib/something.test'
`;

const styleModuleImports = `
import stylesWithMultipleExtensions from './styles.module.scss'
import styles from './styles.scss'
`;

const bareModuleSpecifiers = `
import babel from '@babel/core'
import { transform } from '@babel/core'
export * from '@babel/core'
export { transform } from '@babel/core'
`;

describe("plugin", () => {
test.each`
type | statements
${"module imports"} | ${importStatements}
${"module exports"} | ${exportStatements}
`("should fully qualify the $type specifiers", ({ statements }) => {
const result = transformSync(statements, {
plugins: ["@babel/plugin-syntax-typescript", plugin],
filename: "foo",
configFile: false,
});

expect(result?.code).toMatchSnapshot();
});

test.each`
type | statements
${"type-only imports"} | ${typeOnlyImports}
${"type-only exports"} | ${typeOnlyExports}
${"style module imports"} | ${styleModuleImports}
${"bare module specifiers"} | ${bareModuleSpecifiers}
`("should skip $type", ({ statements }) => {
const result = transformSync(statements, {
plugins: ["@babel/plugin-syntax-typescript", plugin],
filename: "foo",
configFile: false,
});

expect(result?.code).toMatchSnapshot();
});
});
13 changes: 13 additions & 0 deletions packages/babel-plugin-fully-specified/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*!
* Copyright (c) 2021-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

export { plugin as default } from "./plugin";
112 changes: 112 additions & 0 deletions packages/babel-plugin-fully-specified/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*!
* Copyright (c) 2021-present, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/

import type * as Babel from "@babel/core";
import * as BabelTypes from "@babel/types";
import { resolve, extname, dirname } from "path";
import { existsSync, lstatSync } from "fs";

const isNodeModule = (candidate: string): boolean => {
if (candidate.startsWith(".") || candidate.startsWith("/")) {
return false;
}

try {
require.resolve(candidate);
return true;
} catch (e) {
if (e instanceof Error) {
const error: NodeJS.ErrnoException = e;
if (error.code == "MODULE_NOT_FOUND") return false;
throw error;
}
}
return false;
};

const skipModule = (candidate: string) =>
!candidate.startsWith(".") ||
isNodeModule(candidate) ||
[".js", ".scss"].includes(extname(candidate));

export function plugin({
types: t,
}: {
types: typeof BabelTypes;
}): Babel.PluginObj {
return {
name: "fully-specified",
visitor: {
// @ts-expect-error type annotations don't support multiple visitor foo|bar|baz syntactic sugar
"ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration": (
path: Babel.NodePath<
| BabelTypes.ImportDeclaration
| BabelTypes.ExportNamedDeclaration
| BabelTypes.ExportAllDeclaration
>,
state: Babel.PluginPass
): void => {
if (!state.filename) {
return;
}

if (path.node.type === "ImportDeclaration") {
if (path.node.importKind === "type") {
return;
}
} else {
if (path.node.exportKind === "type") {
return;
}
}

const candidate = path.node.source?.value;
if (!candidate) {
return;
}
if (skipModule(candidate)) {
return;
}

const dirPath = resolve(dirname(state.filename), candidate);

const fullySpecifiedLiteral = t.stringLiteral(
existsSync(dirPath) && lstatSync(dirPath).isDirectory()
? `${candidate}${candidate.endsWith("/") ? "" : "/"}index.js`
: `${candidate}.js`
);

switch (path.node.type) {
case "ImportDeclaration":
path.replaceWith(
t.importDeclaration(path.node.specifiers, fullySpecifiedLiteral)
);
return;
case "ExportNamedDeclaration":
path.replaceWith(
t.exportNamedDeclaration(
path.node.declaration,
path.node.specifiers,
fullySpecifiedLiteral
)
);
return;
case "ExportAllDeclaration":
path.replaceWith(t.exportAllDeclaration(fullySpecifiedLiteral));
return;
default:
throw new Error("Invalid path node type for visitor!");
}
},
},
};
}
7 changes: 7 additions & 0 deletions packages/babel-plugin-fully-specified/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "@okta/odyssey-typescript/tsconfig.node12.json",
"include": ["./src"],
"compilerOptions": {
"outDir": "dist"
}
}
1 change: 1 addition & 0 deletions packages/odyssey-react-theme/babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {

env: {
production: {
plugins: ["@okta/fully-specified"],
presets: [
[
"@okta/odyssey-babel-preset",
Expand Down
1 change: 1 addition & 0 deletions packages/odyssey-react/babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {

env: {
production: {
plugins: ["@okta/fully-specified"],
presets: [
[
"@okta/odyssey-babel-preset",
Expand Down

0 comments on commit 8b6493d

Please sign in to comment.