Expose createModuleIdFactory as bundler option

Summary:
**Summary**

`createModuleIdFactory` is already used in `metro` internally, but is currently always a fixed function.
This enables `metro.runBuild()` to be run with a custom module ID factory.

One use case: building a base bundle, on top of which other application-specific bundles could reference modules in the base.  The application-specific IDs need to not conflict with those in the base bundle, and all references to modules in the base must resolve to the correct ID in the base bundle.  A custom ID factory can make all this possible.

**Test plan**

<!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. -->

Using `metro.runBuild(...)` with these changes, I was able to substitute in a custom ID factory

```javascript
const fs = require('fs')
const metro = require('metro')

const baseManifestFileContents = JSON.parse(fs.readFileSync('./baseManifest.json'))
// baseManifest looks like:
// { "modules": { ...
//    "react/index.js": { "id": 12 },
//    "react/cjs/react.production.min.js": { "id": 13 }, ...  } }

const opts = {
  dev: false,
  entry: 'index.js',
  out: './out.bundle',
  platform: 'ios',
  projectRoots: ['.', 'node_modules'],
  config: {
    createModuleIdFactory: createModuleIdFactory(baseManifestFileContents)
  }
}

metro.runBuild(opts)

// Creates a sample custom ID factory
function createModuleIdFactory(manifestFileContents) {
  return function createModuleIdFactory() {
    const fileToIdMap = new Map()
    let nextId = manifestFileContents ? getNextIdAfterBaseManifest(manifestFileContents) : 0

    return path => {
      const sourcePath = path
        .replace(process.cwd() + '/node_modules/', '')
        .replace(process.cwd(), '.')

      // If module is in the base manifest, return its ID
      if (manifestFileContents && manifestFileContents.modules[sourcePath]) {
        return manifestFileContents.modules[sourcePath].id
      }

      // Otherwise, get it from the map or create a new ID
      if (!fileToIdMap.has(path)) {
        fileToIdMap.set(path, nextId)
        nextId += 1
      }
      return fileToIdMap.get(path)
    }
  }

  function getNextIdAfterBaseManifest(manifestFileContents) {
    return Object.keys(manifestFileContents.modules).reduce((id, key) => {
      if (manifestFileContents.modules[key].id > id) {
        return manifestFileContents.modules[key].id
      }
      return id
    }, 0) + 1
  }
}
```

With the sample module ID factory above, the output looks like the following, where defined module IDs start at a higher number to avoid the base module IDs, but may depend on modules in the base bundle (lower numbers).

```javascript
...
__d(function(r,o,t,i,n){t.exports=r.ErrorUtils},551,[]);
__d(function(n,t,o,r,u){'use strict';var e,c=t(u[0]);e=c.now?function(){return c.now()}:function(){return Date.now()},o.exports=e},552,[553]);
...
__d(function(e,t,r,s,l){'use strict'; ...},564,[18,565,27]);
...
```
Closes https://github.com/facebook/metro/pull/100

Reviewed By: mjesun

Differential Revision: D6508351

Pulled By: rafeca

fbshipit-source-id: f2cfe5c373a6c83c8ae6c526435538633a7c9c2a
This commit is contained in:
Adam Liechty 2017-12-08 06:19:58 -08:00 committed by Facebook Github Bot
parent 53290f5b9c
commit f347e4ff47
6 changed files with 111 additions and 1 deletions

View File

@ -139,6 +139,11 @@ export type ConfigT = {
* contain the absolute path of each module. * contain the absolute path of each module.
*/ */
getModulesRunBeforeMainModule: (entryFilePath: string) => Array<string>, getModulesRunBeforeMainModule: (entryFilePath: string) => Array<string>,
/**
* An optional custom module ID factory creator used by the bundler.
*/
createModuleIdFactory?: () => (path: string) => number,
}; };
const DEFAULT = ({ const DEFAULT = ({

View File

@ -108,7 +108,7 @@ class DeltaTransformer extends EventEmitter {
// different bundles to share the same ids (on HMR, where we need to patch // different bundles to share the same ids (on HMR, where we need to patch
// the correct module). // the correct module).
this._getModuleId = this._bundleOptions.isolateModuleIDs this._getModuleId = this._bundleOptions.isolateModuleIDs
? createModuleIdFactory() ? (bundleOptions.createModuleIdFactory || createModuleIdFactory)()
: globalCreateModuleId; : globalCreateModuleId;
this._deltaCalculator.on('change', this._onFileChange); this._deltaCalculator.on('change', this._onFileChange);

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+js_foundation
* @format
*/
'use strict';
jest
.mock('fs')
.mock('assert')
.mock('progress')
.mock('../DeltaCalculator')
.mock('../../JSTransformer')
.mock('/root/to/something.js', () => ({}), {virtual: true})
.mock('/path/to/transformer.js', () => ({}), {virtual: true});
const fs = require('fs');
const Bundler = require('../../Bundler');
const Resolver = require('../../Resolver');
const DeltaTransformer = require('../DeltaTransformer');
const defaults = require('../../defaults');
const bundlerOptions = {
allowBundleUpdates: false,
assetExts: defaults.assetExts,
cacheVersion: 'smth',
enableBabelRCLookup: true,
extraNodeModules: {},
platforms: defaults.platforms,
resetCache: false,
sourceExts: defaults.sourceExts,
transformModulePath: '/path/to/transformer.js',
watch: false,
projectRoots: ['/root'],
assetServer: {
getAssetData: jest.fn(),
},
};
describe('DeltaTransformer', () => {
let bundler;
beforeEach(() => {
Resolver.load = jest
.fn()
.mockImplementation(opts => Promise.resolve(new Resolver(opts)));
fs.__setMockFilesystem({
path: {to: {'transformer.js': ''}},
root: {to: {'something.js': ''}},
});
fs.statSync.mockImplementation(function() {
return {
isDirectory: () => true,
};
});
bundler = new Bundler(bundlerOptions);
});
it('should allow setting a custom module ID factory', async () => {
const bundlerOptions = {
isolateModuleIDs: true,
createModuleIdFactory: createPlus10000ModuleIdFactory,
};
const deltaTransformer = await DeltaTransformer.create(
bundler,
{},
bundlerOptions,
);
expect(deltaTransformer._getModuleId('test/path')).toBe(10000);
});
});
function createPlus10000ModuleIdFactory(): (path: string) => number {
const fileToIdMap: Map<string, number> = new Map();
let nextId = 10000;
return (path: string) => {
let id = fileToIdMap.get(path);
if (typeof id !== 'number') {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}

View File

@ -72,6 +72,7 @@ class Server {
assetExts: Array<string>, assetExts: Array<string>,
blacklistRE: void | RegExp, blacklistRE: void | RegExp,
cacheVersion: string, cacheVersion: string,
createModuleIdFactory?: () => (path: string) => number,
enableBabelRCLookup: boolean, enableBabelRCLookup: boolean,
extraNodeModules: {}, extraNodeModules: {},
getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>, getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
@ -121,6 +122,7 @@ class Server {
assetRegistryPath: options.assetRegistryPath, assetRegistryPath: options.assetRegistryPath,
blacklistRE: options.blacklistRE, blacklistRE: options.blacklistRE,
cacheVersion: options.cacheVersion || '1.0', cacheVersion: options.cacheVersion || '1.0',
createModuleIdFactory: options.createModuleIdFactory,
enableBabelRCLookup: enableBabelRCLookup:
options.enableBabelRCLookup != null options.enableBabelRCLookup != null
? options.enableBabelRCLookup ? options.enableBabelRCLookup

View File

@ -94,6 +94,7 @@ async function runMetro({
assetExts: normalizedConfig.assetTransforms ? [] : assetExts, assetExts: normalizedConfig.assetTransforms ? [] : assetExts,
assetRegistryPath: normalizedConfig.assetRegistryPath, assetRegistryPath: normalizedConfig.assetRegistryPath,
blacklistRE: normalizedConfig.getBlacklistRE(), blacklistRE: normalizedConfig.getBlacklistRE(),
createModuleIdFactory: normalizedConfig.createModuleIdFactory,
extraNodeModules: normalizedConfig.extraNodeModules, extraNodeModules: normalizedConfig.extraNodeModules,
getPolyfills: normalizedConfig.getPolyfills, getPolyfills: normalizedConfig.getPolyfills,
getModulesRunBeforeMainModule: getModulesRunBeforeMainModule:
@ -239,6 +240,9 @@ exports.runBuild = async (options: RunBuildOptions) => {
minify: options.optimize || false, minify: options.optimize || false,
platform: options.platform || `web`, platform: options.platform || `web`,
sourceMapUrl: options.sourceMapUrl, sourceMapUrl: options.sourceMapUrl,
createModuleIdFactory: options.config
? options.config.createModuleIdFactory
: undefined,
}; };
const metroBundle = await MetroBundler.build(metroServer, requestOptions); const metroBundle = await MetroBundler.build(metroServer, requestOptions);

View File

@ -46,6 +46,7 @@ export type BundleOptions = {
+runModule: boolean, +runModule: boolean,
sourceMapUrl: ?string, sourceMapUrl: ?string,
unbundle: boolean, unbundle: boolean,
createModuleIdFactory?: () => (path: string) => number,
}; };
export type ModuleGroups = {| export type ModuleGroups = {|
@ -69,6 +70,7 @@ export type Options = {|
+assetRegistryPath: string, +assetRegistryPath: string,
blacklistRE?: RegExp, blacklistRE?: RegExp,
cacheVersion?: string, cacheVersion?: string,
createModuleIdFactory?: () => (path: string) => number,
enableBabelRCLookup?: boolean, enableBabelRCLookup?: boolean,
extraNodeModules?: {}, extraNodeModules?: {},
getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>, getPolyfills: ({platform: ?string}) => $ReadOnlyArray<string>,
@ -113,4 +115,5 @@ export type RequestOptions = {|
dev?: boolean, dev?: boolean,
minify: boolean, minify: boolean,
platform: string, platform: string,
createModuleIdFactory?: () => (path: string) => number,
|}; |};