UNPKG

create-expo-cljs-app

Version:

Create a react native application with Expo and Shadow-CLJS!

572 lines (514 loc) 16.5 kB
/** * 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;