├── .github ├── CODEOWNERS ├── renovate.json ├── dependabot.disabled.yml └── workflows │ └── nodejs.yml ├── .gitattributes ├── test ├── __fixtures__ │ ├── MyAwesomeApp │ │ ├── node_modules │ │ │ └── @awesome-app │ │ │ │ ├── final-feature │ │ │ │ ├── 1.js │ │ │ │ ├── 2.js │ │ │ │ ├── 0.js │ │ │ │ ├── package.json │ │ │ │ └── index.js │ │ │ │ ├── some-feature │ │ │ │ ├── package.json │ │ │ │ └── index.js │ │ │ │ ├── another-feature │ │ │ │ ├── package.json │ │ │ │ └── index.js │ │ │ │ └── yet-another-feature │ │ │ │ ├── package.json │ │ │ │ └── index.js │ │ └── package.json │ ├── BatchedBridge │ │ ├── package.json │ │ ├── module-01.js │ │ └── module-02.js │ ├── AppRegistry │ │ ├── package.json │ │ ├── component-01.js │ │ └── component-02.js │ └── MyOtherAwesomeApp │ │ └── package.json ├── index.test.ts └── __snapshots__ │ └── index.test.ts.snap ├── src ├── index.js ├── generateLazyIndex.js ├── experiences.js └── module.js ├── .gitignore ├── .vscode └── settings.json ├── tsconfig.json ├── CODE_OF_CONDUCT.md ├── .yarnrc.yml ├── README.md ├── LICENSE ├── package.json └── SECURITY.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tido64 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | /.yarn/releases/* binary 3 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/final-feature/1.js: -------------------------------------------------------------------------------- 1 | require("./0"); 2 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/final-feature/2.js: -------------------------------------------------------------------------------- 1 | require("./1"); 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | codegen.require("./generateLazyIndex"); // eslint-disable-line no-undef 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tgz 2 | .yarn/* 3 | !.yarn/releases/ 4 | coverage/ 5 | node_modules/ 6 | !test/__fixtures__/*/node_modules/ 7 | -------------------------------------------------------------------------------- /test/__fixtures__/BatchedBridge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "experiences": [ 3 | "./module-01", 4 | "./module-02" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/__fixtures__/AppRegistry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "experiences": [ 3 | "./component-01", 4 | "./component-02" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/final-feature/0.js: -------------------------------------------------------------------------------- 1 | This file shouldn't be read so it contains invalid JS. 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[json]": { 6 | "editor.formatOnSave": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/__fixtures__/AppRegistry/component-01.js: -------------------------------------------------------------------------------- 1 | const { AppRegistry } = require("react-native"); 2 | 3 | AppRegistry.registerComponent("Component-01", () => null); 4 | -------------------------------------------------------------------------------- /test/__fixtures__/BatchedBridge/module-01.js: -------------------------------------------------------------------------------- 1 | const { BatchedBridge } = require("react-native"); 2 | 3 | BatchedBridge.registerCallableModule("Module-01", null); 4 | -------------------------------------------------------------------------------- /test/__fixtures__/BatchedBridge/module-02.js: -------------------------------------------------------------------------------- 1 | const { BatchedBridge } = require("react-native"); 2 | require("./module-01"); 3 | 4 | BatchedBridge.registerCallableModule("Module-02", null); 5 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/some-feature/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@awesome-app/some-feature", 3 | "version": "1.0.0-dev", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/__fixtures__/AppRegistry/component-02.js: -------------------------------------------------------------------------------- 1 | const { AppRegistry } = require("react-native"); 2 | require("./component-01"); 3 | 4 | AppRegistry.registerComponent("Component-02", () => null); 5 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/another-feature/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@awesome-app/another-feature", 3 | "version": "1.0.0-dev", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/final-feature/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@awesome-app/final-feature", 3 | "version": "1.0.0-dev", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/yet-another-feature/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@awesome-app/yet-another-feature", 3 | "version": "1.0.0-dev", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "experiences": [ 3 | "@awesome-app/yet-another-feature", 4 | "@awesome-app/another-feature", 5 | "@awesome-app/some-feature", 6 | "@awesome-app/final-feature" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/another-feature/index.js: -------------------------------------------------------------------------------- 1 | require("@awesome-app/yet-another-feature"); 2 | const { BatchedBridge } = require("react-native"); 3 | 4 | BatchedBridge.registerCallableModule("AnotherFeature", null); 5 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/final-feature/index.js: -------------------------------------------------------------------------------- 1 | const { AppRegistry } = require("react-native"); 2 | require("@awesome-app/yet-another-feature"); 3 | require("./2"); 4 | 5 | AppRegistry.registerComponent("FinalFeature", () => null); 6 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", "schedule:earlyMondays"], 3 | "labels": ["dependencies"], 4 | "postUpdateOptions": ["yarnDedupeHighest"], 5 | "rangeStrategy": "update-lockfile", 6 | "stabilityDays": 7, 7 | "timezone": "Europe/Oslo" 8 | } 9 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/some-feature/index.js: -------------------------------------------------------------------------------- 1 | require("@awesome-app/another-feature"); 2 | const { AppRegistry } = require("react-native"); 3 | const { version } = require("./package.json"); 4 | 5 | AppRegistry.registerComponent("SomeFeature", () => version); 6 | -------------------------------------------------------------------------------- /test/__fixtures__/MyAwesomeApp/node_modules/@awesome-app/yet-another-feature/index.js: -------------------------------------------------------------------------------- 1 | const { BatchedBridge } = require("react-native"); 2 | 3 | BatchedBridge.registerCallableModule("YetAnotherFeature", null); 4 | BatchedBridge.registerLazyCallableModule("YetAnotherFeatureLazy", () => null); 5 | -------------------------------------------------------------------------------- /.github/dependabot.disabled.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | versioning-strategy: "lockfile-only" 12 | -------------------------------------------------------------------------------- /test/__fixtures__/MyOtherAwesomeApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "experiences": { 3 | "callable:YetAnotherFeature": "@awesome-app/yet-another-feature", 4 | "callable:YetAnotherFeatureLazy": "@awesome-app/yet-another-feature", 5 | "callable:AnotherFeature": "@awesome-app/another-feature", 6 | "SomeFeature": "@awesome-app/some-feature", 7 | "FinalFeature": "@awesome-app/final-feature" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "allowJs": true, 5 | "checkJs": true, 6 | "noEmit": true, 7 | "strict": true, 8 | "moduleResolution": "Node", 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/*.js", "test/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableScripts: false 2 | enableTelemetry: false 3 | logFilters: 4 | - code: YN0007 # X must be built because it never has been before or the last one failed 5 | level: discard 6 | - code: YN0008 # X must be rebuilt because its dependency tree changed 7 | level: discard 8 | - code: YN0013 # X can't be found in the cache and will be fetched from the remote registry 9 | level: discard 10 | nodeLinker: node-modules 11 | npmRegistryServer: "https://registry.npmjs.org" 12 | yarnPath: .yarn/releases/yarn-3.2.0.cjs 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-lazy-index is now @rnx-kit/react-native-lazy-index 2 | 3 | `react-native-lazy-index` has **moved** to 4 | [rnx-kit](https://github.com/microsoft/rnx-kit/tree/main/packages/react-native-lazy-index), 5 | and has been **renamed** to 6 | [`@rnx-kit/react-native-lazy-index`](https://www.npmjs.com/package/@rnx-kit/react-native-lazy-index). 7 | 8 | To migrate, simply replace `react-native-lazy-index` in `package.json`: 9 | 10 | ```diff 11 | - "react-native-lazy-index": "^2.1.1", 12 | + "@rnx-kit/react-native-lazy-index": "^2.1.1", 13 | ``` 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: 3 | push: 4 | branches: 5 | - trunk 6 | pull_request: 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | node-version: [12, 14, 16] 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v3.0.0 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - name: Checkout 19 | uses: actions/checkout@v3.0.0 20 | - name: Deduplicate packages 21 | if: ${{ matrix.node-version == '14' }} 22 | run: yarn dedupe --check 23 | - name: Cache /.yarn/cache 24 | uses: actions/cache@v2 25 | with: 26 | path: .yarn/cache 27 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-yarn- 30 | - name: Install 31 | run: yarn 32 | - name: Check 33 | if: ${{ matrix.node-version == '14' }} 34 | run: yarn tsc 35 | - name: Lint 36 | if: ${{ matrix.node-version == '14' }} 37 | run: yarn lint 38 | - name: Test 39 | run: yarn test 40 | release: 41 | needs: [test] 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Set up Node.js 45 | uses: actions/setup-node@v3.0.0 46 | with: 47 | node-version: 14 48 | - name: Checkout 49 | uses: actions/checkout@v3.0.0 50 | - name: Cache /.yarn/cache 51 | uses: actions/cache@v2 52 | with: 53 | path: .yarn/cache 54 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} 55 | restore-keys: | 56 | ${{ runner.os }}-yarn- 57 | - name: Install 58 | run: yarn 59 | - name: Release 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | run: npx semantic-release 64 | -------------------------------------------------------------------------------- /src/generateLazyIndex.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | "use strict"; 3 | 4 | const { parseExperiences } = require("./experiences"); 5 | const { resolveModule } = require("./module"); 6 | 7 | /** 8 | * @typedef {{ 9 | * type: "app" | "callable"; 10 | * moduleId: string; 11 | * source: string; 12 | * }} Component; 13 | */ 14 | 15 | /** 16 | * Generates the index file. 17 | * @param {Record} components 18 | * @returns {string} 19 | */ 20 | function generateIndex(components) { 21 | let shouldImportAppRegistry = false; 22 | let shouldImportBatchedBridge = false; 23 | 24 | const lines = Object.keys(components).reduce((index, name) => { 25 | const { type, moduleId, source } = components[name]; 26 | switch (type) { 27 | case "app": 28 | shouldImportAppRegistry = true; 29 | index.push( 30 | `AppRegistry.registerComponent("${name}", () => {`, 31 | ` // Source: ${source}`, 32 | ` require("${moduleId}");`, 33 | ` return AppRegistry.getRunnable("${name}").componentProvider();`, 34 | `});` 35 | ); 36 | break; 37 | case "callable": 38 | shouldImportBatchedBridge = true; 39 | index.push( 40 | `BatchedBridge.registerLazyCallableModule("${name}", () => {`, 41 | ` // Source: ${source}`, 42 | ` require("${moduleId}");`, 43 | ` return BatchedBridge.getCallableModule("${name}");`, 44 | `});` 45 | ); 46 | break; 47 | default: 48 | throw new Error(`Unknown component type: ${type}`); 49 | } 50 | return index; 51 | }, /** @type {string[]} */ ([])); 52 | 53 | if (shouldImportBatchedBridge) { 54 | lines.unshift( 55 | 'const BatchedBridge = require("react-native/Libraries/BatchedBridge/BatchedBridge");' 56 | ); 57 | } 58 | if (shouldImportAppRegistry) { 59 | lines.unshift('const { AppRegistry } = require("react-native");'); 60 | } 61 | 62 | return lines.join("\n"); 63 | } 64 | 65 | module.exports = () => { 66 | const fs = require("fs"); 67 | 68 | const packageManifest = resolveModule("./package.json"); 69 | const { experiences } = JSON.parse(fs.readFileSync(packageManifest, "utf-8")); 70 | 71 | return generateIndex(parseExperiences(experiences)); 72 | }; 73 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import type { BabelFileResult } from "@babel/core"; 2 | 3 | describe("react-native-lazy-index", () => { 4 | const babel = require("@babel/core"); 5 | const { spawnSync } = require("child_process"); 6 | const path = require("path"); 7 | 8 | const currentWorkingDir = process.cwd(); 9 | 10 | /** 11 | * Generates a sequence from RegEx matches. 12 | */ 13 | function* generateSequence( 14 | str: string, 15 | regex: RegExp 16 | ): Generator { 17 | let m = regex.exec(str); 18 | while (m) { 19 | yield m[1]; 20 | m = regex.exec(str); 21 | } 22 | } 23 | 24 | /** 25 | * Tests the specified fixture. 26 | */ 27 | function transformFixture(fixture: string): BabelFileResult | null { 28 | const workingDir = path.join(__dirname, "__fixtures__", fixture); 29 | process.chdir(workingDir); 30 | return babel.transformFileSync("../../../src/index.js", { 31 | cwd: workingDir, 32 | filename: `${fixture}.js`, 33 | plugins: ["codegen"], 34 | }); 35 | } 36 | 37 | afterEach(() => process.chdir(currentWorkingDir)); 38 | 39 | test("wraps AppRegistry.registerComponent calls", () => { 40 | const result = transformFixture("AppRegistry"); 41 | expect(result).toBeTruthy(); 42 | expect(result?.code).toMatchSnapshot(); 43 | }); 44 | 45 | test("wraps BatchedBridge.registerCallableModule calls", () => { 46 | const result = transformFixture("BatchedBridge"); 47 | expect(result).toBeTruthy(); 48 | expect(result?.code).toMatchSnapshot(); 49 | }); 50 | 51 | test("wraps registered components", () => { 52 | const result = transformFixture("MyAwesomeApp"); 53 | expect(result).toBeTruthy(); 54 | expect(result?.code).toMatchSnapshot(); 55 | }); 56 | 57 | test("wraps registered components using declared entry points", () => { 58 | const result = transformFixture("MyOtherAwesomeApp"); 59 | expect(result).toBeTruthy(); 60 | expect(result?.code).toMatchSnapshot(); 61 | }); 62 | 63 | test("packs only necessary files", () => { 64 | const files = Array.from( 65 | generateSequence( 66 | spawnSync("npm", ["pack", "--dry-run"]).output.toString(), 67 | /[.\d]+k?B\s+([^\s]*)/g 68 | ) 69 | ); 70 | expect(files.sort()).toEqual([ 71 | "LICENSE", 72 | "README.md", 73 | "package.json", 74 | "src/experiences.js", 75 | "src/generateLazyIndex.js", 76 | "src/index.js", 77 | "src/module.js", 78 | ]); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-lazy-index", 3 | "version": "2.0.0-dev", 4 | "description": "RAM bundle friendly index.js with on-demand loaded modules", 5 | "keywords": [ 6 | "babel", 7 | "bundle", 8 | "codegen", 9 | "index.js", 10 | "inline", 11 | "lazy", 12 | "macro", 13 | "on-demand", 14 | "performance", 15 | "ram", 16 | "react", 17 | "react-native", 18 | "require" 19 | ], 20 | "homepage": "https://github.com/microsoft/react-native-lazy-index#readme", 21 | "bugs": "https://github.com/microsoft/react-native-lazy-index/issues", 22 | "license": "MIT", 23 | "files": [ 24 | "src/*.js" 25 | ], 26 | "main": "src/index.js", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/microsoft/react-native-lazy-index.git" 30 | }, 31 | "scripts": { 32 | "clean": "git clean -dfqx --exclude=.yarn/cache", 33 | "format": "prettier --write $(git ls-files '*.js' '*.json' '*.ts')", 34 | "lint": "eslint --no-ignore $(git ls-files '*.js' '*.ts' ':!:test/__fixtures__/*/0.js')", 35 | "test": "jest" 36 | }, 37 | "dependencies": { 38 | "@babel/core": "^7.0.0-0", 39 | "@babel/parser": "^7.0.0", 40 | "@babel/traverse": "^7.0.0", 41 | "@babel/types": "^7.0.0", 42 | "babel-plugin-codegen": "^4.0.0" 43 | }, 44 | "peerDependencies": { 45 | "react-native": ">=0.59" 46 | }, 47 | "devDependencies": { 48 | "@babel/cli": "^7.0.0", 49 | "@microsoft/eslint-plugin-sdl": "^0.1.9", 50 | "@rnx-kit/eslint-plugin": "^0.2.0", 51 | "@rnx-kit/jest-preset": "^0.1.0", 52 | "@types/jest": "^27.0.0", 53 | "@types/node": "^16.0.0", 54 | "eslint": "^8.0.0", 55 | "eslint-plugin-jest": "^26.0.0", 56 | "jest": "^27.0.0", 57 | "prettier": "^2.0.0", 58 | "semantic-release": "^19.0.0", 59 | "typescript": "^4.0.0" 60 | }, 61 | "packageManager": "yarn@3.2.0", 62 | "resolutions": { 63 | "eslint-plugin-react": "^7.26.0" 64 | }, 65 | "eslintConfig": { 66 | "extends": [ 67 | "plugin:@microsoft/sdl/required", 68 | "plugin:@rnx-kit/recommended", 69 | "plugin:jest/recommended", 70 | "plugin:jest/style" 71 | ], 72 | "rules": { 73 | "@typescript-eslint/ban-ts-comment": "off" 74 | }, 75 | "settings": { 76 | "react": { 77 | "version": "17.0.2" 78 | } 79 | } 80 | }, 81 | "jest": { 82 | "preset": "@rnx-kit/jest-preset", 83 | "roots": [ 84 | "test" 85 | ], 86 | "testRegex": "/test/.*\\.test\\.ts$" 87 | }, 88 | "release": { 89 | "branches": [ 90 | "trunk" 91 | ], 92 | "tagFormat": "${version}" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/experiences.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | "use strict"; 3 | 4 | const { scanModule } = require("./module"); 5 | 6 | /** 7 | * @typedef {import("./generateLazyIndex").Component} Component; 8 | */ 9 | 10 | const CALLABLE_PREFIX = "callable:"; 11 | const DEFAULT_MAX_DEPTH = 3; 12 | 13 | /** 14 | * Retrieves platform extensions from command line arguments. 15 | * 16 | * TODO: This method needs to be implemented. 17 | * 18 | * @returns {string[]} 19 | */ 20 | function getPlatformExtensions() { 21 | return [".ios", ".android", ".native"]; 22 | } 23 | 24 | /** 25 | * Same as `parseInt()` but with a default value. 26 | * @param {string | undefined} s 27 | * @param {number} defaultValue 28 | */ 29 | function parseIntDefault(s, defaultValue) { 30 | const value = parseInt(s || ""); 31 | return isNaN(value) ? defaultValue : value; 32 | } 33 | 34 | /** 35 | * @param {string[]} experiences 36 | * @returns {Record} 37 | */ 38 | function parseExperiencesFromArray(experiences) { 39 | const depth = parseIntDefault( 40 | process.env["RN_LAZY_INDEX_MAX_DEPTH"], 41 | DEFAULT_MAX_DEPTH 42 | ); 43 | 44 | const platformExtensions = getPlatformExtensions(); 45 | 46 | /** @type {Set} */ 47 | const visited = new Set(); 48 | 49 | const verbose = Boolean(process.env["RN_LAZY_INDEX_VERBOSE"]); 50 | 51 | return experiences.reduce( 52 | (components, module) => 53 | scanModule(components, module, { 54 | module, 55 | depth, 56 | platformExtensions, 57 | visited, 58 | verbose, 59 | }), 60 | /** @type {Record} */ ({}) 61 | ); 62 | } 63 | 64 | /** 65 | * @param {Record} experiences 66 | * @returns {Record} 67 | */ 68 | function parseExperiencesFromObject(experiences) { 69 | return Object.keys(experiences).reduce((components, name) => { 70 | const moduleId = experiences[name]; 71 | if (typeof moduleId !== "string") { 72 | return components; 73 | } 74 | 75 | /** @type {[Component["type"], string]} */ 76 | const [type, id] = name.startsWith(CALLABLE_PREFIX) 77 | ? ["callable", name.slice(CALLABLE_PREFIX.length)] 78 | : ["app", name]; 79 | 80 | components[id] = { type, moduleId, source: "package.json" }; 81 | return components; 82 | }, /** @type {Record} */ ({})); 83 | } 84 | 85 | /** 86 | * @param {string | number | boolean | {} | null | undefined} experiences 87 | * @returns {Record} 88 | */ 89 | function parseExperiences(experiences) { 90 | if (!experiences) { 91 | throw new Error("Missing `experiences` section in `package.json`"); 92 | } 93 | 94 | if (Array.isArray(experiences)) { 95 | return parseExperiencesFromArray(experiences); 96 | } 97 | 98 | if (typeof experiences === "object") { 99 | return parseExperiencesFromObject(experiences); 100 | } 101 | 102 | throw new Error("Invalid `experiences` section in `package.json`"); 103 | } 104 | 105 | exports.parseExperiences = parseExperiences; 106 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`react-native-lazy-index wraps AppRegistry.registerComponent calls 1`] = ` 4 | "// @ts-nocheck 5 | const { 6 | AppRegistry 7 | } = require(\\"react-native\\"); 8 | 9 | AppRegistry.registerComponent(\\"Component-01\\", () => { 10 | // Source: component-01.js 11 | require(\\"./component-01\\"); 12 | 13 | return AppRegistry.getRunnable(\\"Component-01\\").componentProvider(); 14 | }); 15 | AppRegistry.registerComponent(\\"Component-02\\", () => { 16 | // Source: component-02.js 17 | require(\\"./component-02\\"); 18 | 19 | return AppRegistry.getRunnable(\\"Component-02\\").componentProvider(); 20 | });" 21 | `; 22 | 23 | exports[`react-native-lazy-index wraps BatchedBridge.registerCallableModule calls 1`] = ` 24 | "// @ts-nocheck 25 | const BatchedBridge = require(\\"react-native/Libraries/BatchedBridge/BatchedBridge\\"); 26 | 27 | BatchedBridge.registerLazyCallableModule(\\"Module-01\\", () => { 28 | // Source: module-01.js 29 | require(\\"./module-01\\"); 30 | 31 | return BatchedBridge.getCallableModule(\\"Module-01\\"); 32 | }); 33 | BatchedBridge.registerLazyCallableModule(\\"Module-02\\", () => { 34 | // Source: module-02.js 35 | require(\\"./module-02\\"); 36 | 37 | return BatchedBridge.getCallableModule(\\"Module-02\\"); 38 | });" 39 | `; 40 | 41 | exports[`react-native-lazy-index wraps registered components 1`] = ` 42 | "// @ts-nocheck 43 | const { 44 | AppRegistry 45 | } = require(\\"react-native\\"); 46 | 47 | const BatchedBridge = require(\\"react-native/Libraries/BatchedBridge/BatchedBridge\\"); 48 | 49 | BatchedBridge.registerLazyCallableModule(\\"YetAnotherFeature\\", () => { 50 | // Source: node_modules/@awesome-app/yet-another-feature/index.js 51 | require(\\"@awesome-app/yet-another-feature\\"); 52 | 53 | return BatchedBridge.getCallableModule(\\"YetAnotherFeature\\"); 54 | }); 55 | BatchedBridge.registerLazyCallableModule(\\"YetAnotherFeatureLazy\\", () => { 56 | // Source: node_modules/@awesome-app/yet-another-feature/index.js 57 | require(\\"@awesome-app/yet-another-feature\\"); 58 | 59 | return BatchedBridge.getCallableModule(\\"YetAnotherFeatureLazy\\"); 60 | }); 61 | BatchedBridge.registerLazyCallableModule(\\"AnotherFeature\\", () => { 62 | // Source: node_modules/@awesome-app/another-feature/index.js 63 | require(\\"@awesome-app/another-feature\\"); 64 | 65 | return BatchedBridge.getCallableModule(\\"AnotherFeature\\"); 66 | }); 67 | AppRegistry.registerComponent(\\"SomeFeature\\", () => { 68 | // Source: node_modules/@awesome-app/some-feature/index.js 69 | require(\\"@awesome-app/some-feature\\"); 70 | 71 | return AppRegistry.getRunnable(\\"SomeFeature\\").componentProvider(); 72 | }); 73 | AppRegistry.registerComponent(\\"FinalFeature\\", () => { 74 | // Source: node_modules/@awesome-app/final-feature/index.js 75 | require(\\"@awesome-app/final-feature\\"); 76 | 77 | return AppRegistry.getRunnable(\\"FinalFeature\\").componentProvider(); 78 | });" 79 | `; 80 | 81 | exports[`react-native-lazy-index wraps registered components using declared entry points 1`] = ` 82 | "// @ts-nocheck 83 | const { 84 | AppRegistry 85 | } = require(\\"react-native\\"); 86 | 87 | const BatchedBridge = require(\\"react-native/Libraries/BatchedBridge/BatchedBridge\\"); 88 | 89 | BatchedBridge.registerLazyCallableModule(\\"YetAnotherFeature\\", () => { 90 | // Source: package.json 91 | require(\\"@awesome-app/yet-another-feature\\"); 92 | 93 | return BatchedBridge.getCallableModule(\\"YetAnotherFeature\\"); 94 | }); 95 | BatchedBridge.registerLazyCallableModule(\\"YetAnotherFeatureLazy\\", () => { 96 | // Source: package.json 97 | require(\\"@awesome-app/yet-another-feature\\"); 98 | 99 | return BatchedBridge.getCallableModule(\\"YetAnotherFeatureLazy\\"); 100 | }); 101 | BatchedBridge.registerLazyCallableModule(\\"AnotherFeature\\", () => { 102 | // Source: package.json 103 | require(\\"@awesome-app/another-feature\\"); 104 | 105 | return BatchedBridge.getCallableModule(\\"AnotherFeature\\"); 106 | }); 107 | AppRegistry.registerComponent(\\"SomeFeature\\", () => { 108 | // Source: package.json 109 | require(\\"@awesome-app/some-feature\\"); 110 | 111 | return AppRegistry.getRunnable(\\"SomeFeature\\").componentProvider(); 112 | }); 113 | AppRegistry.registerComponent(\\"FinalFeature\\", () => { 114 | // Source: package.json 115 | require(\\"@awesome-app/final-feature\\"); 116 | 117 | return AppRegistry.getRunnable(\\"FinalFeature\\").componentProvider(); 118 | });" 119 | `; 120 | -------------------------------------------------------------------------------- /src/module.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | "use strict"; 3 | 4 | const parser = require("@babel/parser"); 5 | const traverse = require("@babel/traverse").default; 6 | const t = require("@babel/types"); 7 | const fs = require("fs"); 8 | const path = require("path"); 9 | 10 | /** 11 | * @typedef {import("./generateLazyIndex").Component} Component; 12 | * 13 | * @typedef {{ 14 | * module: string; 15 | * parent?: string; 16 | * depth: number; 17 | * platformExtensions: string[]; 18 | * visited: Set; 19 | * verbose: boolean; 20 | * }} ScanState 21 | */ 22 | 23 | const TAG = "react-native-lazy-index"; 24 | 25 | /** 26 | * Ensures that specified node is a StringLiteral. 27 | * @param {import("@babel/types").Node} node 28 | * @param {string} modulePath 29 | * @param {import("@babel/types").Identifier} calleeObject 30 | * @param {import("@babel/types").Identifier} calleeProperty 31 | * @returns {node is import("@babel/types").StringLiteral} 32 | */ 33 | function ensureStringLiteral(node, modulePath, calleeObject, calleeProperty) { 34 | if (!t.isStringLiteral(node)) { 35 | const source = path.relative(process.cwd(), modulePath); 36 | const startLine = node.loc ? node.loc.start.line : 0; 37 | console.warn( 38 | `[${TAG}] ${source}:${startLine}: expected string literal as first argument to ${calleeObject.name}.${calleeProperty.name}()` 39 | ); 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** 47 | * @param {string} moduleId 48 | * @param {string=} parentModule 49 | * @param {string[]=} platformExtensions 50 | * @returns {string} 51 | */ 52 | function resolveModule(moduleId, parentModule, platformExtensions = []) { 53 | const options = { 54 | paths: [ 55 | ...(() => { 56 | if (parentModule) { 57 | return [path.dirname(parentModule)]; 58 | } else if ("paths" in module) { 59 | return module["paths"]; 60 | } else { 61 | return []; 62 | } 63 | })(), 64 | process.cwd(), 65 | ], 66 | }; 67 | const resolution = platformExtensions.reduce((resolution, extension) => { 68 | if (!resolution) { 69 | try { 70 | return require.resolve(moduleId + extension, options); 71 | } catch (e) { 72 | // ignore 73 | } 74 | } 75 | return resolution; 76 | }, ""); 77 | return resolution || require.resolve(moduleId, options); 78 | } 79 | 80 | /** 81 | * Scans specified module for component registrations. 82 | * @param {Record} components 83 | * @param {string} moduleId 84 | * @param {ScanState} state 85 | * @returns {Record} 86 | */ 87 | function scanModule(components, moduleId, state) { 88 | if ( 89 | state.depth === 0 || 90 | moduleId.startsWith("@react-native") || 91 | moduleId === "react" || 92 | moduleId.startsWith("react-native") || 93 | moduleId === "redux" 94 | ) { 95 | // Let's not go down this rabbit hole. 96 | return components; 97 | } 98 | 99 | const paths = state.parent ? [path.dirname(state.parent)] : []; 100 | if (state.verbose) { 101 | console.log(`[${TAG}] Trying to resolve '${moduleId}' from '${paths}'`); 102 | } 103 | 104 | const modulePath = resolveModule( 105 | moduleId, 106 | state.parent, 107 | state.platformExtensions 108 | ); 109 | if (state.visited.has(modulePath)) { 110 | return components; 111 | } 112 | 113 | state.visited.add(modulePath); 114 | if (!/\.m?[jt]sx?$/.test(modulePath)) { 115 | return components; 116 | } 117 | 118 | if (state.verbose) { 119 | console.log(`[${TAG}] Reading ${modulePath}`); 120 | } 121 | 122 | const source = fs.readFileSync(modulePath, { encoding: "utf-8" }); 123 | 124 | /** @type {import("@babel/parser").ParserPlugin[]} */ 125 | const plugins = ["jsx"]; 126 | if (modulePath.endsWith(".ts") || modulePath.endsWith(".tsx")) { 127 | plugins.push("typescript"); 128 | } else if (source.includes("@flow")) { 129 | plugins.push("flow"); 130 | } 131 | 132 | const tree = parser.parse(source, { 133 | sourceType: "module", 134 | plugins, 135 | }); 136 | traverse(tree, { 137 | CallExpression({ node }) { 138 | if (t.isIdentifier(node.callee) && node.callee.name === "require") { 139 | const id = node.arguments[0]; 140 | if (t.isStringLiteral(id)) { 141 | scanModule(components, id.value, { 142 | ...state, 143 | parent: modulePath, 144 | depth: state.depth - 1, 145 | }); 146 | } 147 | return; 148 | } 149 | 150 | if (!t.isMemberExpression(node.callee)) { 151 | return; 152 | } 153 | 154 | const { object, property } = node.callee; 155 | if (!t.isIdentifier(object) || !t.isIdentifier(property)) { 156 | return; 157 | } 158 | 159 | if ( 160 | object.name === "AppRegistry" && 161 | property.name === "registerComponent" 162 | ) { 163 | const [appKey] = node.arguments; 164 | if (!ensureStringLiteral(appKey, modulePath, object, property)) { 165 | return; 166 | } 167 | 168 | components[appKey.value] = { 169 | type: "app", 170 | moduleId: state.module, 171 | source: path.relative(process.cwd(), modulePath), 172 | }; 173 | } else if ( 174 | object.name === "BatchedBridge" && 175 | (property.name === "registerCallableModule" || 176 | property.name === "registerLazyCallableModule") 177 | ) { 178 | const [name] = node.arguments; 179 | if (!ensureStringLiteral(name, modulePath, object, property)) { 180 | return; 181 | } 182 | 183 | components[name.value] = { 184 | type: "callable", 185 | moduleId: state.module, 186 | source: path.relative(process.cwd(), modulePath), 187 | }; 188 | } 189 | }, 190 | ImportDeclaration({ node }) { 191 | scanModule(components, node.source.value, { 192 | ...state, 193 | parent: modulePath, 194 | depth: state.depth - 1, 195 | }); 196 | }, 197 | }); 198 | return components; 199 | } 200 | 201 | exports.resolveModule = resolveModule; 202 | exports.scanModule = scanModule; 203 | --------------------------------------------------------------------------------