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(exoflex): add babel plugin for rewriting imports #341

Closed
wants to merge 12 commits into from
4 changes: 2 additions & 2 deletions packages/exoflex/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
jest.config.js
*.js
node_modules/
lib/
jestPresets/
coverage/
12 changes: 12 additions & 0 deletions packages/exoflex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ For bare React Native project, you also need to install `react-native-vector-ico

If you are using TypeScript, naviflex is built using TypeScript and we shipped it along with the `.d.ts` file, so you do not have to install `@types/exoflex`.

Exoflex includes a babel plugin to rewrite the import statements to save bundle size.
To use, add `exoflex/babel` to `plugins` in your babel config.

```json
{
"plugins": [
...,
"exoflex/babel"
]
}
```

## Available Components

To use this library, it's really advised that you use the `Provider` component to wrap your App.
Expand Down
1 change: 1 addition & 0 deletions packages/exoflex/babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./lib/module/babel/index.js');
4 changes: 4 additions & 0 deletions packages/exoflex/example/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ module.exports = function(api) {
},
},
],
[
require.resolve('../src/babel'),
{ mappings: require.resolve('../lib/mappings.json') },
],
],
};
};
2 changes: 1 addition & 1 deletion packages/exoflex/example/src/examples/SwitchExample.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { Switch, Text, Button } from 'exoflex';
import { Switch, Text } from 'exoflex';

function SwitchExample() {
let [switchValue, setSwitchValue] = useState(false);
Expand Down
35 changes: 10 additions & 25 deletions packages/exoflex/example/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,16 @@ module.exports = async function(env, argv) {
* @see https://stackoverflow.com/questions/57455200/cant-use-hooks-with-my-react-component-library-invariant-violation-invalid-hoo
*/
config.resolve.alias['react'] = path.resolve('./node_modules/react');
config.resolve.alias['@ptomasroos/react-native-multi-slider'] = path.resolve(
'./node_modules/react-native-multi-slider',
);
config.resolve.alias['@unimodules/core'] = path.resolve(
'./node_modules/@unimodules/core',
);
config.resolve.alias['expo-asset'] = path.resolve(
'./node_modules/expo-asset',
);
config.resolve.alias['expo-constants'] = path.resolve(
'./node_modules/expo-constants',
);
config.resolve.alias['expo-font'] = path.resolve('./node_modules/expo-font');
config.resolve.alias['react-native-web/dist/exports'] = path.resolve(
'./node_modules/react-native-web/dist/exports',
);
config.resolve.alias['react-native-svg'] = path.resolve(
'./node_modules/react-native-svg',
);
config.resolve.alias['@expo/vector-icons'] = path.resolve(
'./node_modules/@expo/vector-icons',
);
config.resolve.alias['react-native-calendars'] = path.resolve(
'./node_modules/react-native-calendars',
);

[
'@unimodules/core',
'@expo/vector-icons',
'expo-asset',
'expo-font',
'react-native-web/dist/exports',
].forEach((module) => {
config.resolve.alias[module] = path.resolve('./node_modules/' + module);
});

// Add rule to transform exoflex files before loading it.
config.module.rules = [
Expand Down
1 change: 1 addition & 0 deletions packages/exoflex/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|css|styl)$':
'<rootDir>/test/stubs/asset-stub',
},
watchPathIgnorePatterns: ['__fixtures__\\/[^/]+\\/(output|error)\\.js'],
};
8 changes: 6 additions & 2 deletions packages/exoflex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
"module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts",
"files": [
"lib/"
"lib/",
"babel.js"
],
"author": "KodeFox",
"license": "MIT",
"scripts": {
"prepare": "yarn build",
"build": "yarn bob build",
"build": "yarn bob build && node ./scripts/generate-mappings.js",
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
"lint": "eslint --max-warnings 0 \"src/**/*.{ts,tsx}\"",
Expand Down Expand Up @@ -40,6 +41,7 @@
},
"devDependencies": {
"@babel/preset-typescript": "7.8.3",
"@babel/types": "7.8.3",
"@react-native-community/bob": "0.8.0",
"@testing-library/react": "9.4.0",
"@testing-library/react-hooks": "3.2.1",
Expand All @@ -51,11 +53,13 @@
"@types/react-native": "0.60.4",
"@types/react-native-calendars": "1.20.7",
"@types/react-test-renderer": "16.9.2",
"babel-test": "0.2.3",
"chalk": "2.4.2",
"core-js": "3.6.4",
"eslint": "6.8.0",
"eslint-config-kodefox": "0.1.0",
"jest": "24.9.0",
"jest-file-snapshot": "0.3.8",
"jest-in-case": "1.0.2",
"jest-watch-select-projects": "2.0.0",
"jest-watch-typeahead": "0.4.2",
Expand Down
64 changes: 64 additions & 0 deletions packages/exoflex/scripts/generate-mappings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const path = require('path');
const fs = require('fs');
const types = require('@babel/types');
const parser = require('@babel/parser');

const packageJson = require('../package.json');
const root = path.resolve(__dirname, '..');
const output = path.join(root, 'lib/mappings.json');
const source = fs.readFileSync(path.resolve(root, 'src', 'index.ts'), 'utf8');
const ast = parser.parse(source, {
sourceType: 'module',
plugins: [
'jsx',
'typescript',
'objectRestSpread',
'classProperties',
'asyncGenerators',
],
});
Comment on lines +10 to +19
Copy link
Member

Choose a reason for hiding this comment

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

This is exactly what I need for the vision-ui source transformer! (although I need to use the typescript parser)


const index = packageJson.module;
const relative = (value) =>
path.relative(root, path.resolve(path.dirname(index), value));

const mappings = ast.program.body.reduce((acc, declaration, _, self) => {
if (types.isExportNamedDeclaration(declaration)) {
if (declaration.source) {
declaration.specifiers.forEach((specifier) => {
acc[specifier.exported.name] = {
path: relative(declaration.source.value),
name: specifier.local.name,
};
});
} else {
declaration.specifiers.forEach((specifier) => {
const name = specifier.exported.name;

self.forEach((it) => {
if (
types.isImportDeclaration(it) &&
it.specifiers.some(
(s) =>
types.isImportNamespaceSpecifier(s) &&
s.local.name === specifier.local.name,
)
) {
acc[name] = {
path: relative(it.source.value),
name: '*',
};
}
});
});
}
}

return acc;
}, {});

fs.existsSync(path.dirname(output)) || fs.mkdirSync(path.dirname(output));
fs.writeFileSync(
output,
JSON.stringify({ name: packageJson.name, index, mappings }, null, 2),
);
11 changes: 11 additions & 0 deletions packages/exoflex/src/babel/__fixtures__/rewrite-imports/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Text } from 'react-native';
import {
Provider as ExoflexProvider,
Button,
Label,
NonExistent,
NonExistentSecond as Stuff,
DefaultTheme,
Theme,
RubikSourcesMap,
} from 'exoflex';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Text } from 'react-native';
import { Provider as ExoflexProvider } from "exoflex/lib/module/components";
import { Button } from "exoflex/lib/module/components";
import { Label } from "exoflex/lib/module/components/Typography";
import { NonExistent, NonExistentSecond as Stuff } from "exoflex/lib/module/index.js";
Copy link
Member

Choose a reason for hiding this comment

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

Why is there a .js on the end of this line but not the others?

import { DefaultTheme } from "exoflex/lib/module/constants/themes";
import { Theme } from "exoflex/lib/module/types";
import { RubikSourcesMap } from "exoflex/lib/module/constants/fonts";
23 changes: 23 additions & 0 deletions packages/exoflex/src/babel/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const path = require('path');
const { spawnSync } = require('child_process');
const { create } = require('babel-test');
const { toMatchFile } = require('jest-file-snapshot');

expect.extend({ toMatchFile });

spawnSync('node', [
path.resolve(__dirname, '../../../scripts/generate-mappings.js'),
]);

const { fixtures } = create({
plugins: [
[
require.resolve('../index'),
{ mappings: require.resolve('../../../lib/mappings.json') },
],
],
});

// Right now this test will ran once for each platform.
// In the future, we should make it possible to run platform agnostic test files only once.
fixtures('generate mappings', path.join(__dirname, '..', '__fixtures__'));
63 changes: 63 additions & 0 deletions packages/exoflex/src/babel/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const SKIP = Symbol('SKIP');

module.exports = function rewire(babel, options) {
const t = babel.types;

const { name, index, mappings } = require(options.mappings ||
'../../mappings.json');

return {
visitor: {
ImportDeclaration(path) {
if (path.node.source.value !== name || path.node[SKIP]) {
return;
}

path.node.source.value = `${name}/${index}`;
path.replaceWithMultiple(
path.node.specifiers.reduce((declarations, specifier) => {
const mapping = mappings[specifier.imported.name];

if (mapping) {
const alias = `${name}/${mapping.path}`;
const identifier = t.identifier(specifier.local.name);

let s;

switch (mapping.name) {
case 'default':
s = t.importDefaultSpecifier(identifier);
break;
case '*':
s = t.importNamespaceSpecifier(identifier);
break;
default:
s = t.importSpecifier(identifier, t.identifier(mapping.name));
}

declarations.push(
t.importDeclaration([s], t.stringLiteral(alias)),
);
} else {
const previous = declarations.find(
(d) => d.source.value === path.node.source.value,
);

if (previous) {
previous.specifiers.push(specifier);
} else {
const node = t.importDeclaration([specifier], path.node.source);
node[SKIP] = true;
declarations.push(node);
}
}

return declarations;
}, []),
);

path.requeue();
},
},
};
};
Loading