Skip to content
Permalink
Browse files
feat(build): expose /es as a real es module (#4971)
* write package.json in /es submodule

* add extensions

* jest setup

* update storybook & configure to deal with .js paths

* fix a bunch of remaining imports

* update rollup

* ci(codesandbox): use v12

* storybook: try different babel config

* different config again?

* loose?

* storybook alone

* use babel plugin

* rewrite extension rewriter

* remove all extension changes

* forbid extensions now

* undo jest change

* undo rollup changes

* undo storybook changes

* remove

* remove

* ts

* thing

* chore: update preact

* ci: add ESM check

* clean lockfile

* fix lint

* update bundlesize due to preact change

* update node version a little, for esm support

* update preact to latest

* downgrade preact requirement
  • Loading branch information
Haroenv committed Jan 4, 2022
1 parent 97de35a commit e5b343490921f70736e11a7758bdc7a3aeed6d69
Show file tree
Hide file tree
Showing 20 changed files with 334 additions and 40 deletions.
@@ -23,7 +23,7 @@ aliases:
defaults: &defaults
working_directory: ~/instantsearchjs
docker:
- image: cimg/node:12.14.1
- image: cimg/node:12.22.7

version: 2
jobs:
@@ -47,6 +47,9 @@ jobs:
- run:
name: Type Export
command: yarn run build:types
- run:
name: Test Exports
command: yarn run test:exports
lint:
<<: *defaults
steps:
@@ -1,4 +1,5 @@
{
"sandboxes": ["instantsearchjs-es-template-pcw1k"],
"buildCommand": "build"
"buildCommand": "build",
"node": "12"
}
@@ -81,6 +81,12 @@ module.exports = {
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
},
},
{
files: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.js'],
rules: {
'import/extensions': ['error', 'never'],
},
},
{
files: ['*.js'],
rules: {
@@ -1,6 +1,7 @@
/* eslint-disable import/no-commonjs */

const wrapWarningWithDevCheck = require('./scripts/babel/wrap-warning-with-dev-check');
const extensionResolver = require('./scripts/babel/extension-resolver');

const isCJS = process.env.BABEL_ENV === 'cjs';
const isES = process.env.BABEL_ENV === 'es';
@@ -29,15 +30,20 @@ module.exports = (api) => {
'@babel/plugin-transform-react-constant-elements',
'babel-plugin-transform-react-pure-class-to-function',
wrapWarningWithDevCheck,
(isCJS || isES) && [
'inline-replace-variables',
{
__DEV__: {
type: 'node',
replacement: "process.env.NODE_ENV === 'development'",
},
},
],
...(isCJS || isES
? [
[
'inline-replace-variables',
{
__DEV__: {
type: 'node',
replacement: "process.env.NODE_ENV === 'development'",
},
},
],
extensionResolver,
]
: []),
// this plugin is used to test if we need polyfills, not to actually insert them
// only UMD, since cjs & esm have false positives due to imports
isUMD && [
@@ -25,7 +25,7 @@
"build": "yarn run build:cjs && yarn run build:es && yarn run build:umd && yarn run build:types",
"build:umd": "rm -rf dist && BABEL_ENV=umd rollup --config scripts/rollup/config.js",
"build:cjs": "rm -rf cjs && BABEL_ENV=cjs babel src --extensions '.js,.ts,.tsx' --out-dir cjs/ --ignore 'src/index.es.ts','**/__tests__','**/__mocks__' --quiet",
"build:es": "rm -rf es && BABEL_ENV=es babel src --extensions '.js,.ts,.tsx' --out-dir es/ --ignore 'src/index.es.ts','**/__tests__','**/__mocks__' --quiet && BABEL_ENV=es babel src/index.es.ts --out-file es/index.js --quiet",
"build:es": "rm -rf es && BABEL_ENV=es babel src --extensions '.js,.ts,.tsx' --out-dir es/ --ignore 'src/index.es.ts','**/__tests__','**/__mocks__' --quiet && BABEL_ENV=es babel src/index.es.ts --out-file es/index.js --quiet && echo '{\"type\":\"module\"}' > es/package.json",
"build:types": "./scripts/typescript/extract.js",
"doctoc": "doctoc --no-title --maxlevel 3 README.md CONTRIBUTING.md",
"storybook": "start-storybook --quiet --port 6006 --ci --static-dir .storybook/static",
@@ -43,6 +43,7 @@
"test:e2e:saucelabs": "wdio wdio.saucelabs.conf.js",
"test:size": "bundlesize",
"test:argos": "argos upload functional-tests/screenshots --token $ARGOS_TOKEN || true",
"test:exports": "node test/module/is-es-module.mjs",
"release": "shipjs prepare"
},
"files": [
@@ -59,7 +60,7 @@
"classnames": "^2.2.5",
"@algolia/events": "^4.0.1",
"hogan.js": "^3.0.2",
"preact": "^10.0.0",
"preact": "^10.6.0",
"qs": "^6.5.1 < 6.10",
"search-insights": "^2.1.0"
},
@@ -153,7 +154,7 @@
"bundlesize": [
{
"path": "./dist/instantsearch.production.min.js",
"maxSize": "69.50 kB"
"maxSize": "70.00 kB"
},
{
"path": "./dist/instantsearch.development.js",
@@ -0,0 +1,31 @@
/* eslint-disable import/no-commonjs */

const fs = jest.requireActual('fs');

let mockFiles = new Set();
function __setMockFiles(newMockFiles) {
mockFiles = new Set(newMockFiles);
}

const realStatSync = fs.statSync;
function statSync(pathName, ...args) {
try {
return realStatSync(pathName, ...args);
} catch (e) {
if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) {
if (mockFiles.has(pathName)) {
return {
isFile() {
return true;
},
};
}
}
throw e;
}
}

fs.__setMockFiles = __setMockFiles;
fs.statSync = statSync;

module.exports = fs;
@@ -0,0 +1,134 @@
import { transformAsync } from '@babel/core';

import plugin from '../extension-resolver';
import fs from 'fs';
jest.mock('fs');

describe('babel-plugin-extension-resolver', () => {
const options = {
filename: '/path/to/src/file.js',
configFile: false,
babelrc: false,
plugins: [plugin],
};

afterEach(() => {
jest.resetAllMocks();
});

it('name=babel-plugin-extension-resolver', () =>
expect(plugin({ types: {} }, {}).name).toStrictEqual(
'babel-plugin-extension-resolver'
));

it('ignores empty code', async () => {
expect(await transformAsync('', options)).toHaveProperty('code', '');
});

it('ignores module imports', async () => {
expect(
await transformAsync('import path from "path";', options)
).toHaveProperty('code', 'import path from "path";');
});

it('finds .js files', async () => {
fs.__setMockFiles([
'/path/to/src/other.js',
'/path/to/src/other.ts',
'/path/to/src/other.tsx',
]);

expect(
await transformAsync('import other from "./other";', options)
).toHaveProperty('code', 'import other from "./other.js";');
});

it('finds .ts files', async () => {
fs.__setMockFiles(['/path/to/src/other.ts', '/path/to/src/other.tsx']);

expect(
await transformAsync('import other from "./other";', options)
).toHaveProperty('code', 'import other from "./other.js";');
});

it('finds .tsx files', async () => {
fs.__setMockFiles(['/path/to/src/other.tsx']);

expect(
await transformAsync('import other from "./other";', options)
).toHaveProperty('code', 'import other from "./other.js";');
});

it('finds files in parent directory', async () => {
fs.__setMockFiles(['/path/to/other.js', '/path/to/src/other.js']);

expect(
await transformAsync('import other from "../other";', options)
).toHaveProperty('code', 'import other from "../other.js";');
});

it('finds files in child directory', async () => {
fs.__setMockFiles(['/path/to/src/other.js', '/path/to/src/child/other.js']);

expect(
await transformAsync('import other from "./child/other";', options)
).toHaveProperty('code', 'import other from "./child/other.js";');
});

it('uses index file', async () => {
fs.__setMockFiles(['/path/to/src/other/index.js']);

expect(
await transformAsync('import other from "./other";', options)
).toHaveProperty('code', 'import other from "./other/index.js";');
});

it('works with multiple imports', async () => {
fs.__setMockFiles(['/path/to/src/other.js', '/path/to/src/another.js']);

expect(
await transformAsync(
'import other from "./other";\nimport another from "./another";',
options
)
).toHaveProperty(
'code',
'import other from "./other.js";\nimport another from "./another.js";'
);
});

it('works with export from', async () => {
fs.__setMockFiles(['/path/to/src/other.js']);

expect(
await transformAsync('export * from "./other"', options)
).toHaveProperty('code', 'export * from "./other.js";');
});

it('ignores require()', async () => {
fs.__setMockFiles(['/path/to/src/other.js', '/path/to/src/another.js']);

expect(await transformAsync('require("./other");', options)).toHaveProperty(
'code',
'require("./other");'
);
});

it('ignores other function calls', async () => {
fs.__setMockFiles(['/path/to/src/other.js']);

expect(
await transformAsync('requireOOPS("./other");', options)
).toHaveProperty('code', 'requireOOPS("./other");');
});

it('leaves as-is if file not found', async () => {
fs.__setMockFiles([]);

await expect(() =>
transformAsync('import other from "./other";', options)
).rejects.toThrow(
'/path/to/src/file.js: local import for "./other" could not be resolved'
);
});
});
@@ -0,0 +1,108 @@
/* eslint-disable import/no-commonjs */
// original source: https://github.com/shimataro/babel-plugin-module-extension-resolver
// To create proper ES Modules, the paths imported need to be fully-specified,
// and can't be resolved like is possible with CommonJS. To have large compatibility
// without much hassle, we don't use extensions *inside* the source code, but add
// them with this plugin.

const fs = require('fs');
const path = require('path');

const PLUGIN_NAME = 'babel-plugin-extension-resolver';

const srcExtensions = ['.js', '.ts', '.tsx'];
const dstExtension = '.js';

module.exports = function extensionResolver(babel) {
const { types } = babel;
return {
name: PLUGIN_NAME,
visitor: {
Program: {
enter: (programPath, state) => {
const { filename } = state;
programPath.traverse(
{
ImportDeclaration(declaration) {
handleImportDeclaration(types, declaration, filename);
},
ExportDeclaration(declaration) {
handleExportDeclaration(types, declaration, filename);
},
},
state
);
},
},
},
};
};

function handleImportDeclaration(types, declaration, fileName) {
const source = declaration.get('source');
replaceSource(types, source, fileName);
}

function handleExportDeclaration(types, declaration, fileName) {
const source = declaration.get('source');
if (Array.isArray(source)) {
return;
}
replaceSource(types, source, fileName);
}

function replaceSource(types, source, fileName) {
if (!source.isStringLiteral()) {
return;
}
const sourcePath = source.node.value;
if (sourcePath[0] !== '.') {
return;
}
const baseDir = path.dirname(fileName);
const resolvedPath = resolvePath(baseDir, sourcePath);
const normalizedPath = normalizePath(resolvedPath);
source.replaceWith(types.stringLiteral(normalizedPath));
}

function resolvePath(baseDir, sourcePath) {
for (const title of [sourcePath, path.join(sourcePath, 'index')]) {
const resolvedPath = resolveExtension(baseDir, title);
if (resolvedPath !== null) {
return resolvedPath;
}
}
throw new Error(`local import for "${sourcePath}" could not be resolved`);
}

function resolveExtension(baseDir, sourcePath) {
const absolutePath = path.join(baseDir, sourcePath);
if (isFile(absolutePath)) {
return sourcePath;
}
for (const extension of srcExtensions) {
if (isFile(`${absolutePath}${extension}`)) {
return path.relative(baseDir, `${absolutePath}${dstExtension}`);
}
}
return null;
}

function normalizePath(originalPath) {
let normalizedPath = originalPath;
if (path.sep === '\\') {
normalizedPath = normalizedPath.split(path.sep).join('/');
}
if (normalizedPath[0] !== '.') {
normalizedPath = `./${normalizedPath}`;
}
return normalizedPath;
}

function isFile(pathName) {
try {
return fs.statSync(pathName).isFile();
} catch (err) {
return false;
}
}
@@ -34,7 +34,7 @@ function Panel<TWidget extends UnknownWidgetFactory>(
) {
const [isCollapsed, setIsCollapsed] = useState<boolean>(props.isCollapsed);
const [isControlled, setIsControlled] = useState<boolean>(false);
const bodyRef = useRef<HTMLElement | null>(null);
const bodyRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const node = bodyRef.current;

0 comments on commit e5b3434

Please sign in to comment.