create-expo-cljs-app
Version:
Create a react native application with Expo and Shadow-CLJS!
572 lines (514 loc) • 16.5 kB
Flow
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
'use strict';
const nullthrows = require('nullthrows');
const template = require('@babel/template').default;
import type {NodePath} from '@babel/traverse';
// Type only dependency. This is not a runtime dependency
// eslint-disable-next-line import/no-extraneous-dependencies
import typeof * as Types from '@babel/types';
import type {
Node,
ExportAllDeclaration,
ExportDefaultDeclaration,
ExportNamedDeclaration,
ImportDeclaration,
Statement,
Program,
} from '@babel/types';
type State = {
exportAll: Array<{file: string, loc: ?BabelSourceLocation, ...}>,
exportDefault: Array<{local: string, loc: ?BabelSourceLocation, ...}>,
exportNamed: Array<{
local: string,
remote: string,
loc: ?BabelSourceLocation,
...
}>,
imports: Array<{node: Statement}>,
importDefault: BabelNode,
importAll: BabelNode,
opts: {
importDefault: string,
importAll: string,
resolve: boolean,
out?: {isESModule: boolean, ...},
...
},
...
};
export type Visitors = {|
visitor: {|
ExportAllDeclaration: (
path: NodePath<ExportAllDeclaration>,
state: State,
) => void,
ExportDefaultDeclaration: (
path: NodePath<ExportDefaultDeclaration>,
state: State,
) => void,
ExportNamedDeclaration: (
path: NodePath<ExportNamedDeclaration>,
state: State,
) => void,
ImportDeclaration: (
path: NodePath<ImportDeclaration>,
state: State,
) => void,
Program: {|
enter: (path: NodePath<Program>, state: State) => void,
exit: (path: NodePath<Program>, state: State) => void,
|},
|},
|};
/**
* Produces a Babel template that transforms an "import * as x from ..." or an
* "import x from ..." call into a "const x = importAll(...)" call with the
* corresponding id in it.
*/
const importTemplate = template.statement(`
var LOCAL = IMPORT(FILE);
`);
/**
* Produces a Babel template that transforms an "import {x as y} from ..." into
* "const y = require(...).x" call with the corresponding id in it.
*/
const importNamedTemplate = template.statement(`
var LOCAL = require(FILE).REMOTE;
`);
/**
* Produces a Babel template that transforms an "import ..." into
* "require(...)", which is considered a side-effect call.
*/
const importSideEffectTemplate = template.statement(`
require(FILE);
`);
/**
* Produces an "export all" template that traverses all exported symbols and
* re-exposes them.
*/
const exportAllTemplate = template.statements(`
var REQUIRED = require(FILE);
for (var KEY in REQUIRED) {
exports[KEY] = REQUIRED[KEY];
}
`);
/**
* Produces a "named export" or "default export" template to export a single
* symbol.
*/
const exportTemplate = template.statement(`
exports.REMOTE = LOCAL;
`);
/**
* Flags the exported module as a transpiled ES module. Needs to be kept in 1:1
* compatibility with Babel.
*/
const esModuleExportTemplate = template.statement(`
Object.defineProperty(exports, '__esModule', {value: true});
`);
/**
* Resolution template in case it is requested.
*/
const resolveTemplate = template.expression(`
require.resolve(NODE)
`);
/**
* Enforces the resolution of a path to a fully-qualified one, if set.
*/
function resolvePath<TNode: Node>(
node: TNode,
resolve: boolean,
): BabelNodeExpression | TNode {
if (!resolve) {
return node;
}
return resolveTemplate({
NODE: node,
});
}
declare function withLocation<TNode: BabelNode>(
node: TNode,
loc: ?BabelSourceLocation,
): TNode;
// eslint-disable-next-line no-redeclare
declare function withLocation<TNode: BabelNode>(
node: $ReadOnlyArray<TNode>,
loc: ?BabelSourceLocation,
): Array<TNode>;
// eslint-disable-next-line no-redeclare
function withLocation(node, loc) {
if (Array.isArray(node)) {
return node.map(n => withLocation(n, loc));
}
if (!node.loc) {
return {...node, loc};
}
return node;
}
function importExportPlugin({types: t}: {types: Types, ...}): Visitors {
const {isDeclaration, isVariableDeclaration} = t;
return {
visitor: {
ExportAllDeclaration(
path: NodePath<BabelNodeExportAllDeclaration>,
state: State,
): void {
state.exportAll.push({
file: path.node.source.value,
loc: path.node.loc,
});
path.remove();
},
ExportDefaultDeclaration(
path: NodePath<BabelNodeExportDefaultDeclaration>,
state: State,
): void {
const declaration = path.node.declaration;
const id =
declaration.id || path.scope.generateUidIdentifier('default');
// $FlowFixMe Flow error uncovered by typing Babel more strictly
declaration.id = id;
const loc = path.node.loc;
state.exportDefault.push({
local: id.name,
loc,
});
if (isDeclaration(declaration)) {
path.insertBefore(withLocation(declaration, loc));
} else {
path.insertBefore(
withLocation(
t.variableDeclaration('var', [
t.variableDeclarator(id, declaration),
]),
loc,
),
);
}
path.remove();
},
ExportNamedDeclaration(
path: NodePath<ExportNamedDeclaration>,
state: State,
): void {
if (path.node.exportKind && path.node.exportKind !== 'value') {
return;
}
const declaration = path.node.declaration;
const loc = path.node.loc;
if (declaration) {
if (isVariableDeclaration(declaration)) {
declaration.declarations.forEach(d => {
switch (d.id.type) {
case 'ObjectPattern':
{
const properties = d.id.properties;
properties.forEach(p => {
// $FlowFixMe Flow error uncovered by typing Babel more strictly
const name = p.key.name;
state.exportNamed.push({local: name, remote: name, loc});
});
}
break;
case 'ArrayPattern':
{
const elements = d.id.elements;
elements.forEach(e => {
// $FlowFixMe Flow error uncovered by typing Babel more strictly
const name = e.name;
state.exportNamed.push({local: name, remote: name, loc});
});
}
break;
default:
{
// $FlowFixMe Flow error uncovered by typing Babel more strictly
const name = d.id.name;
state.exportNamed.push({local: name, remote: name, loc});
}
break;
}
});
} else {
const id = declaration.id || path.scope.generateUidIdentifier();
// $FlowFixMe Flow error uncovered by typing Babel more strictly
const name = id.name;
// $FlowFixMe Flow error uncovered by typing Babel more strictly
declaration.id = id;
state.exportNamed.push({local: name, remote: name, loc});
}
path.insertBefore(declaration);
}
const specifiers = path.node.specifiers;
if (specifiers) {
specifiers.forEach(s => {
// $FlowFixMe Flow error uncovered by typing Babel more strictly
const local = s.local;
const remote = s.exported;
if (path.node.source) {
const temp = path.scope.generateUidIdentifier(local.name);
if (local.name === 'default') {
path.insertBefore(
withLocation(
importTemplate({
IMPORT: state.importDefault,
FILE: resolvePath(
nullthrows(path.node.source),
state.opts.resolve,
),
LOCAL: temp,
}),
loc,
),
);
state.exportNamed.push({
local: temp.name,
remote: remote.name,
loc,
});
} else if (remote.name === 'default') {
path.insertBefore(
withLocation(
importNamedTemplate({
FILE: resolvePath(
nullthrows(path.node.source),
state.opts.resolve,
),
LOCAL: temp,
REMOTE: local,
}),
loc,
),
);
state.exportDefault.push({local: temp.name, loc});
} else {
path.insertBefore(
withLocation(
importNamedTemplate({
FILE: resolvePath(
nullthrows(path.node.source),
state.opts.resolve,
),
LOCAL: temp,
REMOTE: local,
}),
loc,
),
);
state.exportNamed.push({
local: temp.name,
remote: remote.name,
loc,
});
}
} else {
if (remote.name === 'default') {
state.exportDefault.push({local: local.name, loc});
} else {
state.exportNamed.push({
local: local.name,
remote: remote.name,
loc,
});
}
}
});
}
path.remove();
},
ImportDeclaration(path: NodePath<ImportDeclaration>, state: State): void {
if (path.node.importKind && path.node.importKind !== 'value') {
return;
}
const file = path.node.source;
const specifiers = path.node.specifiers;
const loc = path.node.loc;
if (!specifiers.length) {
state.imports.push({
node: withLocation(
importSideEffectTemplate({
FILE: resolvePath(file, state.opts.resolve),
}),
loc,
),
});
} else {
let sharedModuleImport = null;
if (
specifiers.filter(
s =>
s.type === 'ImportSpecifier' && s.imported.name !== 'default',
).length > 1
) {
sharedModuleImport = path.scope.generateUidIdentifierBasedOnNode(
file,
);
path.scope.push({
id: sharedModuleImport,
init: withLocation(
t.callExpression(t.identifier('require'), [
resolvePath(file, state.opts.resolve),
]),
loc,
),
});
}
specifiers.forEach(s => {
// $FlowFixMe Flow error uncovered by typing Babel more strictly
const imported = s.imported;
const local = s.local;
switch (s.type) {
case 'ImportNamespaceSpecifier':
state.imports.push({
node: withLocation(
importTemplate({
IMPORT: state.importAll,
FILE: resolvePath(file, state.opts.resolve),
LOCAL: local,
}),
loc,
),
});
break;
case 'ImportDefaultSpecifier':
state.imports.push({
node: withLocation(
importTemplate({
IMPORT: state.importDefault,
FILE: resolvePath(file, state.opts.resolve),
LOCAL: local,
}),
loc,
),
});
break;
case 'ImportSpecifier':
if (imported.name === 'default') {
state.imports.push({
node: withLocation(
importTemplate({
IMPORT: state.importDefault,
FILE: resolvePath(file, state.opts.resolve),
LOCAL: local,
}),
loc,
),
});
} else if (sharedModuleImport != null) {
path.scope.push({
id: local,
init: withLocation(
t.memberExpression(sharedModuleImport, imported),
loc,
),
});
} else {
state.imports.push({
node: withLocation(
importNamedTemplate({
FILE: resolvePath(file, state.opts.resolve),
LOCAL: local,
REMOTE: imported,
}),
loc,
),
});
}
break;
default:
throw new TypeError('Unknown import type: ' + s.type);
}
});
}
path.remove();
},
Program: {
enter(path: NodePath<Program>, state: State): void {
state.exportAll = [];
state.exportDefault = [];
state.exportNamed = [];
state.imports = [];
state.importAll = t.identifier(state.opts.importAll);
state.importDefault = t.identifier(state.opts.importDefault);
},
exit(path: NodePath<Program>, state: State): void {
const body = path.node.body;
// state.imports = [node1, node2, node3, ...nodeN]
state.imports.reverse().forEach((e: {node: Statement}) => {
// import nodes are added to the top of the program body
body.unshift(e.node);
});
state.exportDefault.forEach(
(e: {local: string, loc: ?BabelSourceLocation, ...}) => {
body.push(
withLocation(
exportTemplate({
LOCAL: t.identifier(e.local),
REMOTE: t.identifier('default'),
}),
e.loc,
),
);
},
);
state.exportAll.forEach(
(e: {file: string, loc: ?BabelSourceLocation, ...}) => {
body.push(
...withLocation(
exportAllTemplate({
FILE: resolvePath(
t.stringLiteral(e.file),
state.opts.resolve,
),
REQUIRED: path.scope.generateUidIdentifier(e.file),
KEY: path.scope.generateUidIdentifier('key'),
}),
e.loc,
),
);
},
);
state.exportNamed.forEach(
(e: {
local: string,
remote: string,
loc: ?BabelSourceLocation,
...
}) => {
body.push(
withLocation(
exportTemplate({
LOCAL: t.identifier(e.local),
REMOTE: t.identifier(e.remote),
}),
e.loc,
),
);
},
);
if (
state.exportDefault.length ||
state.exportAll.length ||
state.exportNamed.length
) {
body.unshift(esModuleExportTemplate());
if (state.opts.out) {
state.opts.out.isESModule = true;
}
} else if (state.opts.out) {
state.opts.out.isESModule = false;
}
},
},
},
};
}
module.exports = importExportPlugin;