UNPKG

create-expo-cljs-app

Version:

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

625 lines (536 loc) 17.6 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. * * @format * @flow */ 'use strict'; const invariant = require('invariant'); const nullthrows = require('nullthrows'); const generate = require('@babel/generator').default; const template = require('@babel/template').default; const traverse = require('@babel/traverse').default; const types = require('@babel/types'); const {isImport} = types; import type {NodePath} from '@babel/traverse'; import type {CallExpression, Identifier, StringLiteral} from '@babel/types'; import type { AllowOptionalDependencies, AsyncDependencyType, } from 'metro/src/DeltaBundler/types.flow.js'; type ImportDependencyOptions = $ReadOnly<{ asyncType: AsyncDependencyType, jsResource?: boolean, splitCondition?: NodePath<>, }>; export type Dependency<TSplitCondition> = $ReadOnly<{ data: DependencyData<TSplitCondition>, name: string, }>; type DependencyData<TSplitCondition> = $ReadOnly<{ // If null, then the dependency is synchronous. // (ex. `require('foo')`) asyncType: AsyncDependencyType | null, isOptional?: boolean, // If left unspecified, then the dependency is unconditionally split. splitCondition?: TSplitCondition, locs: Array<BabelSourceLocation>, }>; export type MutableInternalDependency<TSplitCondition> = { ...DependencyData<TSplitCondition>, index: number, name: string, }; export type InternalDependency<TSplitCondition> = $ReadOnly< MutableInternalDependency<TSplitCondition>, >; export type State<TSplitCondition> = { asyncRequireModulePathStringLiteral: ?StringLiteral, dependencyCalls: Set<string>, dependencyRegistry: ModuleDependencyRegistry<TSplitCondition>, dependencyTransformer: DependencyTransformer<TSplitCondition>, dynamicRequires: DynamicRequiresBehavior, dependencyMapIdentifier: ?Identifier, keepRequireNames: boolean, allowOptionalDependencies: AllowOptionalDependencies, }; export type Options<TSplitCondition = void> = $ReadOnly<{ asyncRequireModulePath: string, dependencyMapName?: string, dynamicRequires: DynamicRequiresBehavior, inlineableCalls: $ReadOnlyArray<string>, keepRequireNames: boolean, allowOptionalDependencies: AllowOptionalDependencies, dependencyRegistry?: ModuleDependencyRegistry<TSplitCondition>, dependencyTransformer?: DependencyTransformer<TSplitCondition>, }>; export type CollectedDependencies<+TSplitCondition> = $ReadOnly<{ ast: BabelNodeFile, dependencyMapName: string, dependencies: $ReadOnlyArray<Dependency<TSplitCondition>>, }>; // Registry for the dependency of a module. // Defines when dependencies should be collapsed. // E.g. should a module that's once required optinally and once not // be tretaed as the smae or different dependencies. export interface ModuleDependencyRegistry<+TSplitCondition> { registerDependency( qualifier: ImportQualifier, ): InternalDependency<TSplitCondition>; getDependencies(): Array<InternalDependency<TSplitCondition>>; } export interface DependencyTransformer<-TSplitCondition> { transformSyncRequire( path: NodePath<CallExpression>, dependency: InternalDependency<TSplitCondition>, state: State<TSplitCondition>, ): void; transformImportCall( path: NodePath<>, dependency: InternalDependency<TSplitCondition>, state: State<TSplitCondition>, ): void; transformJSResource( path: NodePath<>, dependency: InternalDependency<TSplitCondition>, state: State<TSplitCondition>, ): void; transformPrefetch( path: NodePath<>, dependency: InternalDependency<TSplitCondition>, state: State<TSplitCondition>, ): void; transformIllegalDynamicRequire( path: NodePath<>, state: State<TSplitCondition>, ): void; } export type DynamicRequiresBehavior = 'throwAtRuntime' | 'reject'; /** * Transform all the calls to `require()` and `import()` in a file into ID- * independent code, and return the list of dependencies. For example, a call * like `require('Foo')` could be transformed to `require(_depMap[3], 'Foo')` * where `_depMap` is provided by the outer scope. As such, we don't need to * know the actual module ID. * * The second argument is only provided for debugging purposes. */ function collectDependencies<TSplitCondition = void>( ast: BabelNodeFile, options: Options<TSplitCondition>, ): CollectedDependencies<TSplitCondition> { const visited = new WeakSet(); const state: State<TSplitCondition> = { asyncRequireModulePathStringLiteral: null, dependencyCalls: new Set(), dependencyRegistry: options.dependencyRegistry ?? new DefaultModuleDependencyRegistry(), dependencyTransformer: options.dependencyTransformer ?? DefaultDependencyTransformer, dependencyMapIdentifier: null, dynamicRequires: options.dynamicRequires, keepRequireNames: options.keepRequireNames, allowOptionalDependencies: options.allowOptionalDependencies, }; const visitor = { CallExpression(path, state): void { if (visited.has(path.node)) { return; } const callee = path.node.callee; const name = callee.type === 'Identifier' ? callee.name : null; if (isImport(callee)) { processImportCall(path, state, { asyncType: 'async', }); return; } if (name === '__prefetchImport' && !path.scope.getBinding(name)) { processImportCall(path, state, { asyncType: 'prefetch', }); return; } if (name === '__jsResource' && !path.scope.getBinding(name)) { processImportCall(path, state, { asyncType: 'async', jsResource: true, }); return; } if ( name === '__conditionallySplitJSResource' && !path.scope.getBinding(name) ) { const args = path.get('arguments'); invariant(Array.isArray(args), 'Expected arguments to be an array'); processImportCall(path, state, { asyncType: 'async', jsResource: true, splitCondition: args[1], }); return; } if ( name != null && state.dependencyCalls.has(name) && !path.scope.getBinding(name) ) { processRequireCall(path, state); visited.add(path.node); } }, ImportDeclaration: collectImports, ExportNamedDeclaration: collectImports, ExportAllDeclaration: collectImports, Program(path, state) { state.asyncRequireModulePathStringLiteral = types.stringLiteral( options.asyncRequireModulePath, ); if (options.dependencyMapName != null) { state.dependencyMapIdentifier = types.identifier( options.dependencyMapName, ); } else { state.dependencyMapIdentifier = path.scope.generateUidIdentifier( 'dependencyMap', ); } state.dependencyCalls = new Set(['require', ...options.inlineableCalls]); }, }; traverse(ast, visitor, null, state); const collectedDependencies = state.dependencyRegistry.getDependencies(); // Compute the list of dependencies. const dependencies = new Array(collectedDependencies.length); for (const {index, name, ...dependencyData} of collectedDependencies) { dependencies[index] = { name, data: dependencyData, }; } return { ast, dependencies, dependencyMapName: nullthrows(state.dependencyMapIdentifier).name, }; } function collectImports<TSplitCondition>( path: NodePath<>, state: State<TSplitCondition>, ): void { if (path.node.source) { registerDependency( state, { name: path.node.source.value, asyncType: null, optional: false, }, path, ); } } function processImportCall<TSplitCondition>( path: NodePath<CallExpression>, state: State<TSplitCondition>, options: ImportDependencyOptions, ): void { const name = getModuleNameFromCallArgs(path); if (name == null) { throw new InvalidRequireCallError(path); } const dep = registerDependency( state, { name, asyncType: options.asyncType, splitCondition: options.splitCondition, optional: isOptionalDependency(name, path, state), }, path, ); const transformer = state.dependencyTransformer; if (options.jsResource) { transformer.transformJSResource(path, dep, state); } else if (options.asyncType === 'async') { transformer.transformImportCall(path, dep, state); } else { transformer.transformPrefetch(path, dep, state); } } function processRequireCall<TSplitCondition>( path: NodePath<CallExpression>, state: State<TSplitCondition>, ): void { const name = getModuleNameFromCallArgs(path); const transformer = state.dependencyTransformer; if (name == null) { if (state.dynamicRequires === 'reject') { throw new InvalidRequireCallError(path); } transformer.transformIllegalDynamicRequire(path, state); return; } const dep = registerDependency( state, { name, asyncType: null, optional: isOptionalDependency(name, path, state), }, path, ); transformer.transformSyncRequire(path, dep, state); } function getNearestLocFromPath(path: NodePath<>): ?BabelSourceLocation { let current = path; while (current && !current.node.loc) { current = current.parentPath; } return current?.node.loc; } export type ImportQualifier = $ReadOnly<{ name: string, asyncType: AsyncDependencyType | null, splitCondition?: NodePath<>, optional: boolean, }>; function registerDependency<TSplitCondition>( state: State<TSplitCondition>, qualifier: ImportQualifier, path: NodePath<>, ): InternalDependency<TSplitCondition> { const dependency = state.dependencyRegistry.registerDependency(qualifier); const loc = getNearestLocFromPath(path); if (loc != null) { dependency.locs.push(loc); } return dependency; } function isOptionalDependency<TSplitCondition>( name: string, path: NodePath<>, state: State<TSplitCondition>, ): boolean { const {allowOptionalDependencies} = state; // The async require module is a 'built-in'. Resolving should never fail -> treat it as non-optional. if (name === state.asyncRequireModulePathStringLiteral?.value) { return false; } const isExcluded = () => Array.isArray(allowOptionalDependencies.exclude) && allowOptionalDependencies.exclude.includes(name); if (!allowOptionalDependencies || isExcluded()) { return false; } // Valid statement stack for single-level try-block: expressionStatement -> blockStatement -> tryStatement let sCount = 0; let p = path; while (p && sCount < 3) { if (p.isStatement()) { if (p.node.type === 'BlockStatement') { // A single-level should have the tryStatement immediately followed BlockStatement // with the key 'block' to distinguish from the finally block, which has key = 'finalizer' return ( p.parentPath != null && p.parentPath.node.type === 'TryStatement' && p.key === 'block' ); } sCount += 1; } p = p.parentPath; } return false; } function getModuleNameFromCallArgs(path: NodePath<CallExpression>): ?string { const expectedCount = path.node.callee.name === '__conditionallySplitJSResource' ? 2 : 1; const args = path.get('arguments'); if (!Array.isArray(args) || args.length !== expectedCount) { throw new InvalidRequireCallError(path); } const result = args[0].evaluate(); if (result.confident && typeof result.value === 'string') { return result.value; } return null; } collectDependencies.getModuleNameFromCallArgs = getModuleNameFromCallArgs; class InvalidRequireCallError extends Error { constructor({node}: any) { const line = node.loc && node.loc.start && node.loc.start.line; super( `Invalid call at line ${line || '<unknown>'}: ${generate(node).code}`, ); } } collectDependencies.InvalidRequireCallError = InvalidRequireCallError; /** * Produces a Babel template that will throw at runtime when the require call * is reached. This makes dynamic require errors catchable by libraries that * want to use them. */ const dynamicRequireErrorTemplate = template.statement(` (function(line) { throw new Error( 'Dynamic require defined at line ' + line + '; not supported by Metro', ); })(LINE) `); /** * Produces a Babel template that transforms an "import(...)" call into a * "require(...)" call to the asyncRequire specified. */ const makeAsyncRequireTemplate = template.statement(` require(ASYNC_REQUIRE_MODULE_PATH)(MODULE_ID, MODULE_NAME) `); const makeAsyncPrefetchTemplate = template.statement(` require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, MODULE_NAME) `); const makeJSResourceTemplate = template.statement(` require(ASYNC_REQUIRE_MODULE_PATH).resource(MODULE_ID, MODULE_NAME) `); const DefaultDependencyTransformer: DependencyTransformer<mixed> = { transformSyncRequire( path: NodePath<CallExpression>, dependency: InternalDependency<mixed>, state: State<mixed>, ): void { const moduleIDExpression = createModuleIDExpression(dependency, state); path.node.arguments = state.keepRequireNames ? [moduleIDExpression, types.stringLiteral(dependency.name)] : [moduleIDExpression]; }, transformImportCall( path: NodePath<>, dependency: InternalDependency<mixed>, state: State<mixed>, ): void { path.replaceWith( makeAsyncRequireTemplate({ ASYNC_REQUIRE_MODULE_PATH: nullthrows( state.asyncRequireModulePathStringLiteral, ), MODULE_ID: createModuleIDExpression(dependency, state), MODULE_NAME: createModuleNameLiteral(dependency), }), ); }, transformJSResource( path: NodePath<>, dependency: InternalDependency<mixed>, state: State<mixed>, ): void { path.replaceWith( makeJSResourceTemplate({ ASYNC_REQUIRE_MODULE_PATH: nullthrows( state.asyncRequireModulePathStringLiteral, ), MODULE_ID: createModuleIDExpression(dependency, state), MODULE_NAME: createModuleNameLiteral(dependency), }), ); }, transformPrefetch( path: NodePath<>, dependency: InternalDependency<mixed>, state: State<mixed>, ): void { path.replaceWith( makeAsyncPrefetchTemplate({ ASYNC_REQUIRE_MODULE_PATH: nullthrows( state.asyncRequireModulePathStringLiteral, ), MODULE_ID: createModuleIDExpression(dependency, state), MODULE_NAME: createModuleNameLiteral(dependency), }), ); }, transformIllegalDynamicRequire(path: NodePath<>, state: State<mixed>): void { path.replaceWith( dynamicRequireErrorTemplate({ LINE: types.numericLiteral(path.node.loc?.start.line ?? 0), }), ); }, }; function createModuleIDExpression( dependency: InternalDependency<mixed>, state: State<mixed>, ) { return types.memberExpression( nullthrows(state.dependencyMapIdentifier), types.numericLiteral(dependency.index), true, ); } function createModuleNameLiteral(dependency: InternalDependency<mixed>) { return types.stringLiteral(dependency.name); } class DefaultModuleDependencyRegistry<TSplitCondition = void> implements ModuleDependencyRegistry<TSplitCondition> { _dependencies: Map<string, InternalDependency<TSplitCondition>> = new Map(); registerDependency( qualifier: ImportQualifier, ): InternalDependency<TSplitCondition> { let dependency: ?InternalDependency<TSplitCondition> = this._dependencies.get( qualifier.name, ); if (dependency == null) { const newDependency: MutableInternalDependency<TSplitCondition> = { name: qualifier.name, asyncType: qualifier.asyncType, locs: [], index: this._dependencies.size, }; if (qualifier.optional) { newDependency.isOptional = true; } dependency = newDependency; this._dependencies.set(qualifier.name, dependency); } else { const original = dependency; dependency = collapseDependencies(original, qualifier); if (original !== dependency) { this._dependencies.set(qualifier.name, dependency); } } return dependency; } getDependencies(): Array<InternalDependency<TSplitCondition>> { return Array.from(this._dependencies.values()); } } function collapseDependencies<TSplitCondition>( dependency: InternalDependency<TSplitCondition>, qualifier: ImportQualifier, ): InternalDependency<TSplitCondition> { let collapsed = dependency; // A previously optionally required dependency was required non-optionaly. // Mark it non optional for the whole module if (collapsed.isOptional && !qualifier.optional) { collapsed = { ...dependency, isOptional: false, }; } // A previously asynchronously (or prefetch) required module was required synchronously. // Make the dependency sync. if (collapsed.asyncType != null && qualifier.asyncType == null) { collapsed = {...dependency, asyncType: null}; } // A prefetched dependency was required async in the module. Mark it as async. if (collapsed.asyncType === 'prefetch' && qualifier.asyncType === 'async') { collapsed = { ...dependency, asyncType: 'async', }; } return collapsed; } module.exports = collectDependencies;