├── .npmrc ├── packages ├── cli │ ├── .npmrc │ ├── jasmine.json │ ├── tsconfig.json │ ├── bin │ │ └── bubblewrap.js │ ├── src │ │ ├── lib │ │ │ ├── cmds │ │ │ │ ├── version.ts │ │ │ │ ├── validate.ts │ │ │ │ ├── update.ts │ │ │ │ ├── install.ts │ │ │ │ ├── merge.ts │ │ │ │ ├── doctor.ts │ │ │ │ ├── updateConfig.ts │ │ │ │ └── fingerprint.ts │ │ │ ├── constants.ts │ │ │ ├── pwaValidationHelper.ts │ │ │ ├── AndroidSdkToolsInstaller.ts │ │ │ ├── Cli.ts │ │ │ ├── JdkInstaller.ts │ │ │ └── config.ts │ │ ├── index.ts │ │ └── spec │ │ │ ├── MockPromptSpec.ts │ │ │ ├── DoctorSpec.ts │ │ │ └── mock │ │ │ └── MockPrompt.ts │ ├── package.json │ └── command_flow.dot ├── core │ ├── .npmrc │ ├── template_project │ │ ├── app │ │ │ ├── .gitignore │ │ │ └── src │ │ │ │ └── main │ │ │ │ ├── java │ │ │ │ ├── DelegationService.java │ │ │ │ ├── Application.java │ │ │ │ └── LauncherActivity.java │ │ │ │ └── res │ │ │ │ ├── xml │ │ │ │ ├── shortcuts.xml │ │ │ │ └── filepaths.xml │ │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ └── strings.xml │ │ │ │ ├── drawable-anydpi │ │ │ │ ├── shortcut_legacy_background.xml │ │ │ │ └── shortcut_monochrome.xml │ │ │ │ ├── drawable-anydpi-v26 │ │ │ │ ├── shortcut_monochrome.xml │ │ │ │ └── shortcut_maskable.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ ├── settings.gradle │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── enable-debug.sh │ │ ├── build.gradle │ │ └── gradlew.bat │ ├── src │ │ ├── spec │ │ │ ├── fixtures │ │ │ │ ├── add_task.png │ │ │ │ └── add_task_coloured.png │ │ │ └── lib │ │ │ │ ├── ConfigSpec.ts │ │ │ │ ├── features │ │ │ │ ├── AppsFlyerFeatureSpec.ts │ │ │ │ ├── FirstRunFlagFeatureSpec.ts │ │ │ │ └── FileHandlingFeatureSpec.ts │ │ │ │ ├── BufferedLogSpec.ts │ │ │ │ ├── TwaGeneratorSpec.ts │ │ │ │ ├── GradleWrapperSpec.ts │ │ │ │ ├── jdk │ │ │ │ └── JarSignerSpec.ts │ │ │ │ ├── ImageHelperSpec.ts │ │ │ │ ├── DigitalAssetLinksSpec.ts │ │ │ │ ├── ResultSpec.ts │ │ │ │ ├── FileHandlerSpec.ts │ │ │ │ └── ShortcutInfoSpec.ts │ │ ├── lib │ │ │ ├── errors │ │ │ │ └── ValidatePathError.ts │ │ │ ├── features │ │ │ │ ├── ArCoreFeature.ts │ │ │ │ ├── LocationDelegationFeature.ts │ │ │ │ ├── ProtocolHandlersFeature.ts │ │ │ │ ├── EmptyFeature.ts │ │ │ │ ├── FirstRunFlagFeature.ts │ │ │ │ ├── FileHandlingFeature.ts │ │ │ │ ├── PlayBillingFeature.ts │ │ │ │ ├── AppsFlyerFeature.ts │ │ │ │ └── Feature.ts │ │ │ ├── DigitalAssetLinks.ts │ │ │ ├── wrappers │ │ │ │ └── svg2img.ts │ │ │ ├── jdk │ │ │ │ └── JarSigner.ts │ │ │ ├── Config.ts │ │ │ ├── mock │ │ │ │ └── MockLog.ts │ │ │ ├── types │ │ │ │ ├── FileHandler.ts │ │ │ │ ├── WebManifest.ts │ │ │ │ └── ProtocolHandler.ts │ │ │ ├── GradleWrapper.ts │ │ │ ├── BufferedLog.ts │ │ │ ├── Result.ts │ │ │ ├── FetchUtils.ts │ │ │ ├── ShortcutInfo.ts │ │ │ └── Log.ts │ │ └── index.ts │ ├── jasmine.json │ ├── tsconfig.json │ ├── package.json │ └── README.md └── validator │ ├── .npmrc │ ├── jasmine.json │ ├── tsconfig.json │ ├── src │ ├── index.ts │ ├── lib │ │ └── psi │ │ │ ├── index.ts │ │ │ ├── PsiResult.ts │ │ │ └── PageSpeedInsights.ts │ └── spec │ │ └── psi │ │ └── PageSpeedInsightsSpec.ts │ ├── package.json │ ├── README.md │ └── package-lock.json ├── lerna.json ├── .eslintignore ├── .gitignore ├── publishing.md ├── Dockerfile ├── .github ├── workflows │ ├── nodejs.yml │ └── docker.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── CONTRIBUTING.md ├── .eslintrc.json ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/cli/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/core/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/validator/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/core/template_project/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /packages/core/template_project/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.24.1" 6 | } 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist/ 3 | packages/linter/ 4 | jdk8u232-b09/ 5 | android_sdk/ 6 | 7 | -------------------------------------------------------------------------------- /packages/core/src/spec/fixtures/add_task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/bubblewrap/main/packages/core/src/spec/fixtures/add_task.png -------------------------------------------------------------------------------- /packages/core/src/spec/fixtures/add_task_coloured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/bubblewrap/main/packages/core/src/spec/fixtures/add_task_coloured.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | ._jdk8u232-b09 4 | jdk8u232-b09 5 | android_sdk 6 | dist 7 | **/node_modules 8 | **/dist 9 | lerna-debug.log 10 | **/.gradle 11 | -------------------------------------------------------------------------------- /packages/core/template_project/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/bubblewrap/main/packages/core/template_project/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /publishing.md: -------------------------------------------------------------------------------- 1 | # Publishing 2 | 3 | To publish a new build: 4 | 5 | ```bash 6 | npm login 7 | # Make sure your code is in the state you want it. 8 | npm run build 9 | npm run publish 10 | ``` 11 | -------------------------------------------------------------------------------- /packages/cli/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "dist/spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": true 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "dist/spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": true 11 | } 12 | -------------------------------------------------------------------------------- /packages/validator/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "dist/spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": true 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "es2018", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "dist", 8 | "strict": true 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/template_project/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 6 | networkTimeout=10000 7 | validateDistributionUrl=true 8 | -------------------------------------------------------------------------------- /packages/validator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "es2018", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "dist", 8 | "strict": true 9 | }, 10 | "include": ["src/**/*.ts", "src/spec/**/*.ts"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "es2018", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "outDir": "dist", 9 | "strict": true 10 | }, 11 | "include": ["src/**/*.ts"], 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-bullseye 2 | 3 | RUN npm install -g svg2img @bubblewrap/cli 4 | 5 | RUN set -xe \ 6 | && apt update \ 7 | && apt install -y openjdk-17-jre openjdk-17-jdk lib32stdc++6 lib32z1 \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | RUN set -xe \ 11 | && mkdir -p /root/.bubblewrap \ 12 | && echo '{ "jdkPath": "/usr/lib/jvm/java-17-openjdk-amd64", "androidSdkPath": "" }' > /root/.bubblewrap/config.json 13 | 14 | RUN yes | bubblewrap doctor 15 | 16 | WORKDIR /app 17 | 18 | ENTRYPOINT ["bubblewrap"] 19 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/java/DelegationService.java: -------------------------------------------------------------------------------- 1 | package <%= packageId %>; 2 | 3 | <% for(const imp of delegationService.imports) { %> 4 | import <%= imp %>; 5 | <% } %> 6 | 7 | public class DelegationService extends 8 | com.google.androidbrowserhelper.trusted.DelegationService { 9 | @Override 10 | public void onCreate() { 11 | super.onCreate(); 12 | 13 | <% for(const code of delegationService.onCreate) { %> 14 | <%= code %> 15 | <% } %> 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /packages/core/src/lib/errors/ValidatePathError.ts: -------------------------------------------------------------------------------- 1 | export type ErrorCode = 'PathIsNotCorrect' | 'PathIsNotSupported'; 2 | 3 | /** 4 | * Extends Error type to have an error code in addition to the Error's message. 5 | */ 6 | export class ValidatePathError extends Error { 7 | private errorCode: ErrorCode; 8 | 9 | constructor(message: string, errorCode: ErrorCode) { 10 | super(message); 11 | this.errorCode = errorCode; 12 | } 13 | 14 | getErrorCode(): ErrorCode { 15 | return this.errorCode; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x, 20.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm run lint 25 | npm test 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /packages/cli/bin/bubblewrap.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright 2019 Google Inc. All Rights Reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | 'use strict'; 20 | 21 | require('../dist')(); 22 | -------------------------------------------------------------------------------- /packages/validator/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from './lib/PwaValidator'; 18 | export * from './lib/psi/PsiResult'; 19 | -------------------------------------------------------------------------------- /packages/validator/src/lib/psi/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from './PageSpeedInsights'; 18 | export * from './PsiResult'; 19 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/xml/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | #F5F5F5 18 | 19 | -------------------------------------------------------------------------------- /packages/core/template_project/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | android.useAndroidX=true 15 | -------------------------------------------------------------------------------- /packages/validator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bubblewrap/validator", 3 | "version": "1.22.0", 4 | "description": "Validate if an app using Trusted Web Activity fulfills the quality criteria", 5 | "engines": { 6 | "node": ">=14.15.0" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "clean": "del dist", 11 | "lint": "eslint \"src/**/*.{js,ts}\"", 12 | "test": "tsc && jasmine --config=jasmine.json" 13 | }, 14 | "main": "./dist/index.js", 15 | "types": "./dist/index.d.ts", 16 | "files": [ 17 | "dist/lib", 18 | "dist/index.d.ts", 19 | "dist/index.js" 20 | ], 21 | "keywords": [], 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/GoogleChromeLabs/bubblewrap.git" 25 | }, 26 | "author": "", 27 | "license": "Apache-2.0", 28 | "dependencies": { 29 | "@types/node-fetch": "^2.5.7", 30 | "node-fetch": "^2.6.0" 31 | }, 32 | "gitHead": "ba2a8430ee11d6bf650ed3d071c9c144fff31538" 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /packages/core/template_project/enable-debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Copyright 2019 Google Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # Check if at least 1 argument is provided 20 | if [[ $# -eq 0 ]] 21 | then 22 | echo "Usage: 'enable-debug.sh '" 23 | exit 1 24 | fi 25 | 26 | # Invokes ADB and creates the file with the command line 27 | adb shell "echo '_ --disable-digital-asset-link-verification-for-url=\"$1\"' > /data/local/tmp/chrome-command-line" 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/ConfigSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Config} from '../../lib/Config'; 18 | 19 | describe('config', () => { 20 | describe('#constructor', () => { 21 | it('Assigns values correctly', () => { 22 | const config: Config = new Config('/jdk', '/androidSdk'); 23 | expect(config.jdkPath).toBe('/jdk'); 24 | expect(config.androidSdkPath).toBe('/androidSdk'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/ArCoreFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {EmptyFeature} from './EmptyFeature'; 18 | import {Metadata} from './Feature'; 19 | 20 | export interface ArCoreConfig { 21 | enabled: boolean; 22 | } 23 | 24 | export class ArCoreFeature extends EmptyFeature { 25 | constructor() { 26 | super('ArCoreFeature'); 27 | this.androidManifest.applicationMetadata.push(new Metadata('com.google.ar.core', 'required')); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/drawable-anydpi/shortcut_legacy_background.xml: -------------------------------------------------------------------------------- 1 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/java/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package <%= packageId %>; 17 | 18 | <% for(const imp of applicationClass.imports) { %> 19 | import <%= imp %>; 20 | <% } %> 21 | 22 | public class Application extends android.app.Application { 23 | 24 | <% for(const variable of applicationClass.variables) { %> 25 | <%= variable %> 26 | <% } %> 27 | 28 | @Override 29 | public void onCreate() { 30 | super.onCreate(); 31 | <% for(const code of applicationClass.onCreate) { %> 32 | <%= code %> 33 | <% } %> 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/validator/README.md: -------------------------------------------------------------------------------- 1 | 17 | # Bubblewrap Validator 18 | 19 | Bubblewrap Validator is a JavaScript library that helps developers to verify if their 20 | [Trusted Web Activity][1] project is well formed as well as validating if the Progressive Web 21 | App(PWA) used inside it matches the [minimum quality criteria][2]. 22 | 23 | [1]: https://developer.chrome.com/docs/android/trusted-web-activity/ 24 | [2]: https://web.dev/using-a-pwa-in-your-android-app/#quality-criteria 25 | 26 | ## Requirements 27 | - [Node.js](https://nodejs.org/en/) 14.15.0 or above 28 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from 'fs'; 18 | import * as path from 'path'; 19 | import {Log, ConsoleLog} from '@bubblewrap/core'; 20 | 21 | export async function version(log: Log = new ConsoleLog('version')): Promise { 22 | const packageJsonFile = path.join(__dirname, '../../../package.json'); 23 | const packageJsonContents = await (await fs.promises.readFile(packageJsonFile)).toString(); 24 | const packageJson = JSON.parse(packageJsonContents); 25 | log.info(packageJson.version); 26 | return true; 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/drawable-anydpi-v26/shortcut_monochrome.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/drawable-anydpi/shortcut_monochrome.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /packages/core/src/lib/DigitalAssetLinks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class DigitalAssetLinks { 18 | static generateAssetLinks(applicationId: string, ...sha256Fingerprints: string[]): string { 19 | if (sha256Fingerprints.length === 0) { 20 | return '[]'; 21 | } 22 | 23 | return `[{ 24 | "relation": ["delegate_permission/common.handle_all_urls"], 25 | "target": { 26 | "namespace": "android_app", 27 | "package_name": "${applicationId}", 28 | "sha256_cert_fingerprints": ["${sha256Fingerprints.join('\", \"')}"] 29 | } 30 | }]\n`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/lib/wrappers/svg2img.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Resvg, ResvgRenderOptions} from '@resvg/resvg-js'; 18 | 19 | export enum Format { 20 | jpeg = 'jpeg', 21 | jpg = 'jpg', 22 | png = 'png', 23 | } 24 | 25 | export async function svg2img(svg: string): Promise { 26 | const opt = { 27 | fitTo: { 28 | mode: 'width', 29 | value: 1200, // Generate the SVG with 1200px width, for larger icons. 30 | }, 31 | } as ResvgRenderOptions; 32 | const resvg = new Resvg(svg, opt); 33 | const pngData = resvg.render(); 34 | const pngBuffer = pngData.asPng(); 35 | return pngBuffer; 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/template_project/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 18 | 19 | buildscript { 20 | 21 | repositories { 22 | google() 23 | jcenter() 24 | } 25 | dependencies { 26 | classpath 'com.android.tools.build:gradle:8.9.1' 27 | 28 | // NOTE: Do not place your application dependencies here; they belong 29 | // in the individual module build.gradle files 30 | } 31 | } 32 | 33 | allprojects { 34 | repositories { 35 | google() 36 | jcenter() 37 | } 38 | } 39 | 40 | task clean(type: Delete) { 41 | delete rootProject.buildDir 42 | } 43 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "google", 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "parserOptions": { 6 | "ecmaVersion": 2017, 7 | "sourceType": "module" 8 | }, 9 | "rules": { 10 | "max-len": [2, 100, { 11 | "ignoreComments": true, 12 | "ignoreUrls": true, 13 | "tabWidth": 2 14 | }], 15 | "no-implicit-coercion": [2, { 16 | "boolean": false, 17 | "number": true, 18 | "string": true 19 | }], 20 | "no-unused-expressions": [2, { 21 | "allowShortCircuit": true, 22 | "allowTernary": false 23 | }], 24 | "no-unused-vars": [2, { 25 | "vars": "all", 26 | "args": "after-used", 27 | "argsIgnorePattern": "(^reject$|^_$)", 28 | "varsIgnorePattern": "(^_$)" 29 | }], 30 | "quotes": [2, "single"], 31 | "require-jsdoc": 0, 32 | "valid-jsdoc": 0, 33 | "prefer-arrow-callback": 1, 34 | "no-var": 1 35 | }, 36 | "env": { 37 | "node": true, 38 | "jasmine": true 39 | }, 40 | "overrides": [ 41 | { 42 | "files": ["**/*.ts"], 43 | "extends": [ 44 | "plugin:@typescript-eslint/eslint-recommended", 45 | "plugin:@typescript-eslint/recommended" 46 | ], 47 | "rules": { 48 | "@typescript-eslint/no-non-null-assertion": 0 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/features/AppsFlyerFeatureSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {AppsFlyerFeature, AppsFlyerConfig} from '../../../lib/features/AppsFlyerFeature'; 18 | 19 | describe('AppsFlyerFeature', () => { 20 | describe('#constructor', () => { 21 | // variables is the only field dynamically generated. 22 | it('Generates correct variables for application', () => { 23 | const config = { 24 | enabled: true, 25 | appsFlyerId: '12345', 26 | } as AppsFlyerConfig; 27 | const appsFlyerFeature = new AppsFlyerFeature(config); 28 | expect(appsFlyerFeature.applicationClass.variables) 29 | .toEqual(['private static final String AF_DEV_KEY = "12345";']); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "version": "0.4.3", 5 | "description": "Generate TWA projects from a Web Manifest", 6 | "engines": { 7 | "node": ">=14.15.0" 8 | }, 9 | "scripts": { 10 | "build": "lerna run build", 11 | "lint": "lerna run lint", 12 | "test": "lerna run test", 13 | "clean-modules": "lerna clean -y", 14 | "clean": "lerna run clean", 15 | "postinstall": "lerna bootstrap && lerna link", 16 | "publish": "lerna publish", 17 | "publish-canary": "lerna publish --canary" 18 | }, 19 | "keywords": [ 20 | "twa", 21 | "trusted-web-activities", 22 | "pwa", 23 | "progressive-web-apps", 24 | "android" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/GoogleChromeLabs/bubblewrap.git" 29 | }, 30 | "author": "", 31 | "license": "Apache-2.0", 32 | "devDependencies": { 33 | "@types/jasmine": "^3.6.3", 34 | "@types/mock-fs": "^4.13.0", 35 | "@typescript-eslint/eslint-plugin": "^2.34.0", 36 | "@typescript-eslint/parser": "^2.34.0", 37 | "del-cli": "^3.0.1", 38 | "eslint": "^6.6.0", 39 | "eslint-config-google": "^0.14.0", 40 | "jasmine": "^3.6.4", 41 | "lerna": "^6.5.1", 42 | "lerna-changelog": "^2.2.0", 43 | "mock-fs": "^5.4.1", 44 | "npm": "^9.5.1", 45 | "ts-node": "^8.10.2", 46 | "typescript": "^5.5.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/validator/src/spec/psi/PageSpeedInsightsSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {PsiRequestBuilder} from '../../lib/psi'; 18 | 19 | describe('PsiRequestBuilder', () => { 20 | describe('#build', () => { 21 | it('builds a correct request', () => { 22 | const psiRequest = new PsiRequestBuilder(new URL('https://example.com')) 23 | .addCategory('pwa') 24 | .addCategory('performance') 25 | .setStrategy('mobile') 26 | .build(); 27 | const expectedUrl = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed?' + 28 | 'url=https%3A%2F%2Fexample.com%2F&category=pwa&category=performance&strategy=mobile'; 29 | expect(psiRequest.url.toString()).toBe(expectedUrl); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/cli/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {magenta} from 'colors'; 18 | 19 | export const APP_NAME = 'bubblewrap-cli'; 20 | export const ASSETLINKS_OUTPUT_FILE = './assetlinks.json'; 21 | export const BUBBLEWRAP_LOGO = magenta( 22 | /* eslint-disable indent */ 23 | `,-----. ,--. ,--. ,--. 24 | | |) /_,--.,--| |-.| |-.| |,---.,--. ,--,--.--.,--,--.,---. 25 | | .-. | || | .-. | .-. | | .-. | |.'.| | .--' ,-. | .-. | 26 | | '--' ' '' | \`-' | \`-' | \\ --| .'. | | \\ '-' | '-' ' 27 | \`------' \`----' \`---' \`---'\`--'\`----'--' '--\`--' \`--\`--| |-' 28 | \`--\' `); 29 | /* eslint-enable indent */ 30 | export const TWA_MANIFEST_FILE_NAME = './twa-manifest.json'; 31 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Cli} from './lib/Cli'; 18 | import {ConsoleLog} from '@bubblewrap/core'; 19 | 20 | module.exports = async (): Promise => { 21 | const cli = new Cli(); 22 | const log = new ConsoleLog('cli'); 23 | const args = process.argv.slice(2); 24 | 25 | let success; 26 | try { 27 | success = await cli.run(args); 28 | } catch (err) { 29 | if (err instanceof Error) { 30 | log.error(err.message); 31 | } 32 | success = false; 33 | } 34 | 35 | // If running the command fails, we terminate the process signaling an error has occured. 36 | // This helps if the CLI is being used as part of a build process and depends on its result 37 | // to abort the build. 38 | if (!success) { 39 | process.exit(1); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/features/FirstRunFlagFeatureSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {FirstRunFlagConfig, FirstRunFlagFeature} from '../../../lib/features/FirstRunFlagFeature'; 18 | 19 | describe('FirstRunFlagFeature', () => { 20 | describe('#constructor', () => { 21 | it('Generates the correct variables', () => { 22 | const paramName = 'my_param_name'; 23 | // variables is the only field dynamically generated. 24 | const config = { 25 | enabled: true, 26 | queryParameterName: paramName, 27 | } as FirstRunFlagConfig; 28 | const appsFlyerFeature = new FirstRunFlagFeature(config); 29 | expect(appsFlyerFeature.launcherActivity.variables) 30 | .toContain(`private static final String PARAM_FIRST_OPEN = "${paramName}";`); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bubblewrap/cli", 3 | "version": "1.24.1", 4 | "description": "CLI tool to Generate TWA projects from a Web Manifest", 5 | "engines": { 6 | "node": ">=14.15.0" 7 | }, 8 | "bin": { 9 | "bubblewrap": "bin/bubblewrap.js" 10 | }, 11 | "scripts": { 12 | "build": "tsc", 13 | "clean": "del dist", 14 | "lint": "eslint \"src/**/*.{js,ts}\"", 15 | "test": "tsc && jasmine --config=jasmine.json" 16 | }, 17 | "files": [ 18 | "dist/lib", 19 | "dist/index.d.ts", 20 | "dist/index.js", 21 | "bin", 22 | "package.json" 23 | ], 24 | "keywords": [], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/GoogleChromeLabs/bubblewrap.git" 28 | }, 29 | "author": "", 30 | "license": "Apache-2.0", 31 | "dependencies": { 32 | "@bubblewrap/core": "^1.24.1", 33 | "@bubblewrap/validator": "^1.22.0", 34 | "@types/cli-progress": "^3.7.0", 35 | "@types/color": "^3.0.0", 36 | "@types/inquirer": "^8.2.6", 37 | "@types/mime-types": "^2.1.0", 38 | "@types/minimist": "^1.2.0", 39 | "@types/semver": "^7.3.1", 40 | "@types/valid-url": "^1.0.3", 41 | "cli-progress": "^3.8.2", 42 | "color": "^3.1.2", 43 | "colors": "1.4.0", 44 | "inquirer": "^8.2.6", 45 | "mime-types": "^2.1.27", 46 | "minimist": "^1.2.2", 47 | "semver": "^7.3.2", 48 | "valid-url": "^1.0.9" 49 | }, 50 | "gitHead": "ba2a8430ee11d6bf650ed3d071c9c144fff31538" 51 | } 52 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/drawable-anydpi-v26/shortcut_maskable.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 24 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/validate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {PwaValidator} from '@bubblewrap/validator'; 18 | import {ConsoleLog} from '@bubblewrap/core'; 19 | import {ParsedArgs} from 'minimist'; 20 | import {printValidationResult} from '../pwaValidationHelper'; 21 | 22 | const log = new ConsoleLog('validate'); 23 | 24 | /** 25 | * Runs the PwaValidator to check a given URL agains the Quality criteria. More information on the 26 | * Quality Criteria available at: https://web.dev/using-a-pwa-in-your-android-app/#quality-criteria 27 | * @param {ParsedArgs} args 28 | */ 29 | export async function validate(args: ParsedArgs): Promise { 30 | log.info('Validating URL: ', args.url); 31 | const validationResult = await PwaValidator.validate(new URL(args.url)); 32 | printValidationResult(validationResult, log); 33 | return validationResult.status === 'PASS'; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 16 | 18 | 19 | 20 | 21 | 25 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | 11 | env: 12 | IMAGE_NAME: bubblewrap 13 | 14 | jobs: 15 | build: 16 | permissions: 17 | packages: write 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Build image 24 | run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" 25 | 26 | - name: Log in to registry 27 | run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin 28 | 29 | - name: Push image 30 | run: | 31 | IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME 32 | 33 | # Change all uppercase to lowercase 34 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') 35 | # Strip git ref prefix from version 36 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 37 | # Strip "v" prefix from tag name 38 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') 39 | [ "$VERSION" == "main" ] && VERSION=next 40 | echo IMAGE_ID=$IMAGE_ID 41 | echo VERSION=$VERSION 42 | [ "$VERSION" == "merge" ] && exit 0 43 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && docker tag $IMAGE_NAME $IMAGE_ID:latest && docker push $IMAGE_ID:latest 44 | docker tag $IMAGE_NAME $IMAGE_ID:$VERSION 45 | docker push $IMAGE_ID:$VERSION 46 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/LocationDelegationFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {EmptyFeature} from './EmptyFeature'; 18 | 19 | export type LocationDelegationConfig = { 20 | enabled: boolean; 21 | } 22 | 23 | export class LocationDelegationFeature extends EmptyFeature { 24 | constructor() { 25 | super('locationDelegation'); 26 | this.buildGradle.dependencies.push('com.google.androidbrowserhelper:locationdelegation:1.1.2'); 27 | 28 | this.androidManifest.components.push(``); 30 | 31 | this.delegationService.imports.push('com.google.androidbrowserhelper.locationdelegation' + 32 | '.LocationDelegationExtraCommandHandler'); 33 | this.delegationService.onCreate = 34 | 'registerExtraCommandHandler(new LocationDelegationExtraCommandHandler());'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/BufferedLogSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {BufferedLog} from '../../lib/BufferedLog'; 18 | import {MockLog} from '../../lib/mock/MockLog'; 19 | 20 | describe('buffered log', () => { 21 | it('Prints nothing before flush', () => { 22 | const mockLog = new MockLog(); 23 | const bufferedLog = new BufferedLog(mockLog); 24 | 25 | bufferedLog.debug('1'); 26 | bufferedLog.info('2'); 27 | bufferedLog.warn('3'); 28 | bufferedLog.error('4'); 29 | 30 | expect(mockLog.getReceivedData()).toEqual([]); 31 | }); 32 | 33 | it('Prints logs after flush', () => { 34 | const mockLog = new MockLog(); 35 | const bufferedLog = new BufferedLog(mockLog); 36 | 37 | bufferedLog.debug('1'); 38 | bufferedLog.info('2'); 39 | bufferedLog.warn('3'); 40 | bufferedLog.error('4'); 41 | 42 | bufferedLog.flush(); 43 | 44 | expect(mockLog.getReceivedData()).toEqual(['1', '2', '3', '4']); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bubblewrap/core", 3 | "version": "1.24.1", 4 | "description": "Core Library to generate, build and sign TWA projects", 5 | "engines": { 6 | "node": ">=14.15.0" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "clean": "del dist", 11 | "lint": "eslint \"src/**/*.{js,ts}\"", 12 | "test": "tsc && jasmine --config=jasmine.json" 13 | }, 14 | "main": "./dist/index.js", 15 | "types": "./dist/index.d.ts", 16 | "keywords": [ 17 | "twa", 18 | "trusted-web-activities", 19 | "pwa", 20 | "progressive-web-apps", 21 | "android" 22 | ], 23 | "files": [ 24 | "dist/lib", 25 | "dist/index.d.ts", 26 | "dist/index.js", 27 | "template_project" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/GoogleChromeLabs/bubblewrap.git" 32 | }, 33 | "author": "", 34 | "license": "Apache-2.0", 35 | "dependencies": { 36 | "@resvg/resvg-js": "^2.2.0", 37 | "@types/color": "^3.0.0", 38 | "@types/extract-zip": "^1.6.2", 39 | "@types/gapi.client.androidpublisher": "^3.0.0", 40 | "@types/inquirer": "^9.0.7", 41 | "@types/lodash": "^4.17.14", 42 | "@types/mime-types": "^2.1.0", 43 | "@types/node": "^12.20.1", 44 | "@types/node-fetch": "^2.5.10", 45 | "@types/tar": "^4.0.4", 46 | "@types/valid-url": "^1.0.3", 47 | "canvg": "4.0.3", 48 | "color": "^3.1.3", 49 | "extract-zip": "^1.7.0", 50 | "fetch-h2": "^2.5.1", 51 | "googleapis": "^81.0.0", 52 | "inquirer": "^9.0.7", 53 | "jimp": "^0.22.7", 54 | "lodash": "^4.17.21", 55 | "mime-types": "^2.1.28", 56 | "node-fetch": "^2.6.1", 57 | "tar": "^6.1.0", 58 | "valid-url": "^1.0.9" 59 | }, 60 | "gitHead": "ba2a8430ee11d6bf650ed3d071c9c144fff31538" 61 | } 62 | -------------------------------------------------------------------------------- /packages/core/src/lib/jdk/JarSigner.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {JdkHelper} from './JdkHelper'; 18 | import {executeFile} from '../util'; 19 | import {SigningKeyInfo} from '../../lib/TwaManifest'; 20 | 21 | const JARSIGNER_CMD = 'jarsigner'; 22 | const SIGNATURE_ALGORITHM = 'SHA256withRSA'; 23 | const DIGEST_ALGORITHM = 'SHA-256'; 24 | 25 | /** 26 | * Wraps the Java `jarsigner` CLI tool. 27 | */ 28 | export class JarSigner { 29 | constructor(private jdkHelper: JdkHelper) {} 30 | 31 | /** 32 | * Signs a file 33 | */ 34 | async sign(signingKeyInfo: SigningKeyInfo, storepass: string, keypass: string, 35 | inputFile: string, outputFile: string): Promise { 36 | const env = this.jdkHelper.getEnv(); 37 | await executeFile(JARSIGNER_CMD, [ 38 | '-verbose', 39 | '-sigalg', 40 | SIGNATURE_ALGORITHM, 41 | '-digestalg', 42 | DIGEST_ALGORITHM, 43 | '-keystore', 44 | signingKeyInfo.path, 45 | inputFile, 46 | signingKeyInfo.alias, 47 | '-storepass', 48 | storepass, 49 | '-keypass', 50 | keypass, 51 | '-signedjar', 52 | outputFile, 53 | ], env); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /packages/core/src/lib/Config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {promises as fs} from 'fs'; 18 | import {dirname} from 'path'; 19 | 20 | export class Config { 21 | jdkPath: string; 22 | androidSdkPath: string; 23 | 24 | constructor(jdkPath: string, androidSdkPath: string) { 25 | this.jdkPath = jdkPath; 26 | this.androidSdkPath = androidSdkPath; 27 | } 28 | 29 | serialize(): string { 30 | return JSON.stringify(this); 31 | } 32 | 33 | async saveConfig(path: string): Promise { 34 | await fs.mkdir(dirname(path), {recursive: true}); 35 | await fs.writeFile(path, this.serialize()); 36 | } 37 | 38 | static deserialize(data: string): Config { 39 | const config = JSON.parse(data); 40 | return new Config(config.jdkPath, config.androidSdkPath); 41 | } 42 | 43 | static async loadConfig(path: string): Promise { 44 | try { 45 | const data = await fs.readFile(path, 'utf8'); 46 | return Config.deserialize(data); 47 | } catch (err) { 48 | if (err instanceof Error) { 49 | // If config file does not exist 50 | if ((err as NodeJS.ErrnoException).code === 'ENOENT') return undefined; 51 | } 52 | throw err; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/update.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Prompt, InquirerPrompt} from '../Prompt'; 18 | import {ParsedArgs} from 'minimist'; 19 | import {enUS as messages} from '../strings'; 20 | import {updateProject} from './shared'; 21 | 22 | /** 23 | * Updates an existing TWA Project using the `twa-manifest.json`. 24 | * @param {string} [args.manifest] directory where the command should look for the 25 | * `twa-manifest.json`. Defaults to the current folder. 26 | * @param {boolean} [args.skipVersionUpgrade] Skips upgrading appVersionCode and appVersionName 27 | * if set to true. 28 | * @param {string} [args.appVersionName] Value to be used for appVersionName when upgrading 29 | * versions. Ignored if `args.skipVersionUpgrade` is set to true. 30 | */ 31 | export async function update( 32 | args: ParsedArgs, prompt: Prompt = new InquirerPrompt()): Promise { 33 | const targetDirectory = args.directory; 34 | const manifestFile = args.manifest; 35 | 36 | const updated = await updateProject(args.skipVersionUpgrade, args.appVersionName, 37 | prompt, targetDirectory, manifestFile); 38 | if (updated) { 39 | prompt.printMessage(messages.messageProjectBuildReminder); 40 | } 41 | return updated; 42 | } 43 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/install.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {AndroidSdkTools, Config, JdkHelper, Log, ConsoleLog} from '@bubblewrap/core'; 18 | import {ParsedArgs} from 'minimist'; 19 | 20 | const APK_FILE_PARAM = '--apkFile'; 21 | const DEFAULT_APK_FILE = './app-release-signed.apk'; 22 | 23 | const PARAMETERS_TO_IGNORE = ['--verbose', '-r']; 24 | 25 | export async function install( 26 | args: ParsedArgs, config: Config, log: Log = new ConsoleLog('install')): Promise { 27 | const jdkHelper = new JdkHelper(process, config); 28 | const androidSdkTools = await AndroidSdkTools.create(process, config, jdkHelper, log); 29 | const apkFile = args.apkFile || DEFAULT_APK_FILE; 30 | if (args.verbose) { 31 | log.setVerbose(true); 32 | } 33 | 34 | // parameter 0 would be the path to 'node', followed by `bubblewrap.js` at 1, then `install` at 35 | // 2. So, we want to start collecting args from parameter 3 and ignore any a possible 36 | // `--apkFile`, which is specific to install. Extra parameters are passed through to `adb`. 37 | const originalArgs = process.argv.slice(3).filter( 38 | (v) => !v.startsWith(APK_FILE_PARAM) && PARAMETERS_TO_IGNORE.indexOf(v) < 0); 39 | await androidSdkTools.install(apkFile, originalArgs); 40 | return true; 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/features/FileHandlingFeatureSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {FileHandlingFeature} from '../../../lib/features/FileHandlingFeature'; 18 | 19 | describe('FileHandlingFeature', () => { 20 | describe('#constructor', () => { 21 | it('Populates templates from the twaManifest correctly', () => { 22 | const fileHandlers = [ 23 | { 24 | 'actionUrl': 'https://pwa-directory.com/', 25 | 'mimeTypes': ['text/plain'], 26 | }, 27 | { 28 | 'actionUrl': 'https://pwa-directory.com/?image', 29 | 'mimeTypes': ['image/jpg', 'image/jpeg'], 30 | }, 31 | ]; 32 | const fileHandlingFeature = new FileHandlingFeature(fileHandlers); 33 | const activityAliases = fileHandlingFeature.androidManifest.components; 34 | expect(activityAliases).toHaveSize(2); 35 | expect(activityAliases[0].match(//g)).toHaveSize(1); 36 | expect(activityAliases[1].match(//g)).toHaveSize(2); 37 | expect(fileHandlingFeature.buildGradle.configs).toEqual([ 38 | 'resValue "string", "fileHandlingActionUrl0", "https://pwa-directory.com/"', 39 | 'resValue "string", "fileHandlingActionUrl1", "https://pwa-directory.com/?image"', 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/ProtocolHandlersFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {EmptyFeature} from './EmptyFeature'; 18 | import {ProtocolHandler} from '../types/ProtocolHandler'; 19 | 20 | export class ProtocolHandlersFeature extends EmptyFeature { 21 | constructor(protocolHandlers: ProtocolHandler[]) { 22 | super('protocolHandlers'); 23 | if (protocolHandlers.length === 0) return; 24 | for (const handler of protocolHandlers) { 25 | this.androidManifest.launcherActivityEntries.push( 26 | ` 27 | 28 | 29 | 30 | 31 | `, 32 | ); 33 | } 34 | this.launcherActivity.imports.push( 35 | 'java.util.HashMap', 36 | 'java.util.Map', 37 | ); 38 | const mapEntries = new Array(); 39 | for (const handler of protocolHandlers) { 40 | mapEntries.push( 41 | `registry.put("${handler.protocol}", Uri.parse("${handler.url}"));`, 42 | ); 43 | } 44 | this.launcherActivity.methods.push( 45 | `@Override 46 | protected Map getProtocolHandlers() { 47 | Map registry = new HashMap<>(); 48 | ${mapEntries.join('\n')} 49 | return registry; 50 | }`); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/merge.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import {ParsedArgs} from 'minimist'; 19 | import {util, TwaManifest} from '@bubblewrap/core'; 20 | import {updateVersions} from './shared'; 21 | 22 | /** 23 | * Updates an existing TWA Project using the `twa-manifest.json`. 24 | * @param {string} [args.fieldsToIgnore] the fields that shouldn't be updated. 25 | */ 26 | export async function merge(args: ParsedArgs): Promise { 27 | // If there is nothing to ignore, continue with an empty list. 28 | const fieldsToIgnore = args.ignore || []; 29 | const manifestPath = path.join(process.cwd(), 'twa-manifest.json'); 30 | const twaManifest = await TwaManifest.fromFile(manifestPath); 31 | const webManifestUrl: URL = twaManifest.webManifestUrl!; 32 | const webManifest = await util.getWebManifest(webManifestUrl); 33 | const newTwaManifest = 34 | await TwaManifest.merge(fieldsToIgnore, webManifestUrl, webManifest, twaManifest); 35 | 36 | // Update the app (args are not relevant in this case, because update's default values 37 | // are valid for it. We just send something as an input). 38 | if (!args.skipVersionUpgrade) { 39 | const newVersionInfo = 40 | await updateVersions(newTwaManifest, args.appVersionName || twaManifest.appVersionName); 41 | newTwaManifest.appVersionName = newVersionInfo.appVersionName; 42 | newTwaManifest.appVersionCode = newVersionInfo.appVersionCode; 43 | } 44 | 45 | await newTwaManifest.saveToFile(manifestPath); 46 | return true; 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/EmptyFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Feature, Metadata} from './Feature'; 18 | 19 | export class EmptyFeature implements Feature { 20 | name: string; 21 | buildGradle: { 22 | repositories: string[]; 23 | dependencies: string[]; 24 | configs: string[]; 25 | } = { 26 | repositories: new Array(), 27 | dependencies: new Array(), 28 | configs: new Array(), 29 | }; 30 | 31 | androidManifest: { 32 | permissions: string[]; 33 | components: string[]; 34 | applicationMetadata: Metadata[]; 35 | launcherActivityEntries: string[]; 36 | } = { 37 | permissions: new Array(), 38 | components: new Array(), 39 | applicationMetadata: new Array(), 40 | launcherActivityEntries: new Array(), 41 | }; 42 | 43 | applicationClass: { 44 | imports: string[]; 45 | variables: string[]; 46 | onCreate?: string; 47 | } = { 48 | imports: new Array(), 49 | variables: new Array(), 50 | }; 51 | 52 | launcherActivity: { 53 | imports: string[]; 54 | variables: string[]; 55 | methods: string[]; 56 | launchUrl?: string; 57 | } = { 58 | imports: new Array(), 59 | variables: new Array(), 60 | methods: new Array(), 61 | }; 62 | 63 | delegationService: { 64 | imports: string[]; 65 | onCreate?: string; 66 | } = { 67 | imports: new Array(), 68 | }; 69 | 70 | constructor(name: string) { 71 | this.name = name; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/FirstRunFlagFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {EmptyFeature} from './EmptyFeature'; 18 | 19 | export interface FirstRunFlagConfig { 20 | enabled: boolean; 21 | queryParameterName: string; 22 | } 23 | 24 | export class FirstRunFlagFeature extends EmptyFeature { 25 | constructor(config: FirstRunFlagConfig) { 26 | super('firstRunFlag'); 27 | this.launcherActivity.imports.push( 28 | 'android.content.SharedPreferences', 29 | 'android.os.StrictMode'); 30 | this.launcherActivity.methods.push( 31 | `private boolean checkAndMarkFirstOpen() { 32 | StrictMode.ThreadPolicy originalPolicy = StrictMode.allowThreadDiskReads(); 33 | try { 34 | SharedPreferences preferences = getPreferences(MODE_PRIVATE); 35 | boolean isFirstRun = preferences.getBoolean(KEY_FIRST_OPEN, true); 36 | preferences.edit().putBoolean(KEY_FIRST_OPEN, false).apply(); 37 | return isFirstRun; 38 | } finally { 39 | StrictMode.setThreadPolicy(originalPolicy); 40 | } 41 | }`); 42 | this.launcherActivity.variables.push( 43 | 'private static final String KEY_FIRST_OPEN = "bubblewrap.first_open";', 44 | `private static final String PARAM_FIRST_OPEN = "${config.queryParameterName}";`); 45 | this.launcherActivity.launchUrl = 46 | `uri = uri 47 | .buildUpon() 48 | .appendQueryParameter(PARAM_FIRST_OPEN, String.valueOf(checkAndMarkFirstOpen())) 49 | .build();`; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/cli/src/lib/pwaValidationHelper.ts: -------------------------------------------------------------------------------- 1 | import {PwaValidationResult, ScoreResult} from '@bubblewrap/validator'; 2 | import {red, green, bold, underline, yellow} from 'colors'; 3 | import {Log} from '@bubblewrap/core'; 4 | 5 | function getColor(score: ScoreResult): string { 6 | switch (score.status) { 7 | case 'PASS': return green(score.printValue); 8 | case 'WARN': return yellow(score.printValue); 9 | case 'FAIL': return red(score.printValue); 10 | default: return score.printValue; 11 | } 12 | } 13 | 14 | export function printValidationResult(validationResult: PwaValidationResult, log: Log): void { 15 | log.info(''); 16 | log.info('Check the full PageSpeed Insights report at:'); 17 | log.info(`- ${validationResult.psiWebUrl}`); 18 | log.info(''); 19 | 20 | const performanceValue = getColor(validationResult.scores.performance); 21 | const pwaValue = getColor(validationResult.scores.pwa); 22 | 23 | const overallStatus = validationResult.status === 'PASS' ? 24 | green(validationResult.status) : red(validationResult.status); 25 | 26 | const accessibilityValue = validationResult.scores.accessibility.printValue; 27 | 28 | const lcpValue = getColor(validationResult.scores.largestContentfulPaint); 29 | const fidValue = getColor(validationResult.scores.firstInputDelay); 30 | const clsValue = getColor(validationResult.scores.cumulativeLayoutShift); 31 | 32 | log.info(''); 33 | log.info(underline('Quality Criteria scores')); 34 | log.info(`Lighthouse Performance score: ................... ${performanceValue}`); 35 | log.info(`Lighthouse PWA check: ........................... ${pwaValue}`); 36 | log.info(''); 37 | log.info(underline('Web Vitals')); 38 | log.info(`Largest Contentful Paint (LCP) .................. ${lcpValue}`); 39 | log.info(`Maximum Potential First Input Delay (Max FID) ... ${fidValue}`); 40 | log.info(`Cumulative Layout Shift (CLS) ................... ${clsValue}`); 41 | log.info(''); 42 | log.info(underline('Other scores')); 43 | log.info(`Lighthouse Accessibility score................... ${accessibilityValue}`); 44 | log.info(''); 45 | log.info(underline('Summary')); 46 | log.info(bold(`Overall result: ................................. ${overallStatus}`)); 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {AndroidSdkTools} from './lib/androidSdk/AndroidSdkTools'; 18 | import {BufferedLog} from './lib/BufferedLog'; 19 | import {Config} from './lib/Config'; 20 | import {GooglePlay, PlayStoreTrack, asPlayStoreTrack, PlayStoreTracks} from './lib/GooglePlay'; 21 | import {GradleWrapper} from './lib/GradleWrapper'; 22 | import {Log, ConsoleLog} from './lib/Log'; 23 | import {MockLog} from './lib/mock/MockLog'; 24 | import {JarSigner} from './lib/jdk/JarSigner'; 25 | import {JdkHelper} from './lib/jdk/JdkHelper'; 26 | import {KeyTool} from './lib/jdk/KeyTool'; 27 | import {TwaManifest, DisplayModes, DisplayMode, asDisplayMode, Orientation, Orientations, 28 | asOrientation, SigningKeyInfo, Fingerprint} 29 | from './lib/TwaManifest'; 30 | import {TwaGenerator} from './lib/TwaGenerator'; 31 | import {DigitalAssetLinks} from './lib/DigitalAssetLinks'; 32 | import * as util from './lib/util'; 33 | import {Result} from './lib/Result'; 34 | import {fetchUtils} from './lib/FetchUtils'; 35 | 36 | export { 37 | AndroidSdkTools, 38 | BufferedLog, 39 | Config, 40 | DigitalAssetLinks, 41 | fetchUtils, 42 | Fingerprint, 43 | GooglePlay, 44 | GradleWrapper, 45 | JarSigner, 46 | JdkHelper, 47 | KeyTool, 48 | Log, 49 | ConsoleLog, 50 | MockLog, 51 | Orientation, 52 | Orientations, 53 | asOrientation, 54 | TwaGenerator, 55 | TwaManifest, 56 | DisplayMode, 57 | DisplayModes, 58 | asDisplayMode, 59 | util, 60 | Result, 61 | SigningKeyInfo, 62 | PlayStoreTrack, 63 | asPlayStoreTrack, 64 | PlayStoreTracks, 65 | }; 66 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/FileHandlingFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {EmptyFeature} from './EmptyFeature'; 18 | import {FileHandler} from '../types/FileHandler'; 19 | 20 | const activityAliasTemplate = (handler: FileHandler, index: number): string => ` 21 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | ${ handler.mimeTypes.map((mimeType: string) => ` 33 | `, 34 | ).join('') } 35 | 36 | 37 | `; 38 | 39 | export class FileHandlingFeature extends EmptyFeature { 40 | constructor(fileHandlers: FileHandler[]) { 41 | super('fileHandling'); 42 | if (fileHandlers.length === 0) return; 43 | for (let i = 0; i < fileHandlers.length; i++) { 44 | this.androidManifest.components.push(activityAliasTemplate(fileHandlers[i], i)); 45 | this.buildGradle.configs.push( 46 | `resValue "string", "fileHandlingActionUrl${i}", "${fileHandlers[i].actionUrl}"`); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/lib/mock/MockLog.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Log} from '../../lib/Log'; 18 | 19 | /** 20 | * An utility class for testing used for get the logging messages without printing to the user. 21 | */ 22 | export class MockLog implements Log { 23 | private receivedData: Array; 24 | 25 | /** 26 | * Creates a new MockLog instance. 27 | */ 28 | constructor() { 29 | this.receivedData = []; 30 | } 31 | 32 | getReceivedData(): Array { 33 | return this.receivedData; 34 | } 35 | 36 | /** 37 | * Saves the debug message to the Logger. 38 | * @param message The message the be saved. 39 | */ 40 | debug(message: string): void { 41 | this.receivedData.push(message); 42 | } 43 | 44 | /** 45 | * Saves the debug message to the Logger. 46 | * @param message The message the be saved. 47 | */ 48 | info(message: string): void { 49 | this.receivedData.push(message); 50 | } 51 | 52 | /** 53 | * Saves the debug message to the Logger. 54 | * @param message The message the be saved. 55 | */ 56 | warn(message: string): void { 57 | this.receivedData.push(message); 58 | } 59 | 60 | /** 61 | * Saves the debug message to the Logger. 62 | * @param message The message the be saved. 63 | */ 64 | error(message: string): void { 65 | this.receivedData.push(message); 66 | } 67 | 68 | setVerbose(): void { 69 | // Not implemented for testing. 70 | } 71 | 72 | /** 73 | * Creates a new MockLog. 74 | */ 75 | newLog(): Log { 76 | return new MockLog(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/doctor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {ConsoleLog, Log, Config, AndroidSdkTools, JdkHelper} from '@bubblewrap/core'; 18 | import {loadOrCreateConfig} from '../config'; 19 | import {enUS as messages} from '../strings'; 20 | 21 | async function jdkDoctor(config: Config, log: Log): Promise { 22 | const result = await JdkHelper.validatePath(config.jdkPath); 23 | if (result.isError()) { 24 | if (result.unwrapError().getErrorCode() === 'PathIsNotCorrect') { 25 | log.error(messages.jdkPathIsNotCorrect); 26 | return false; 27 | } else if (result.unwrapError().getErrorCode() === 'PathIsNotSupported') { 28 | log.error(messages.jdkIsNotSupported); 29 | return false; 30 | } else { // Error while reading the file, will print the error message. 31 | log.error(result.unwrapError().message); 32 | return false; 33 | } 34 | } 35 | return true; 36 | } 37 | 38 | async function androidSdkDoctor(config: Config, log: Log): Promise { 39 | if ((await AndroidSdkTools.validatePath(config.androidSdkPath)).isError()) { 40 | log.error(messages.androidSdkPathIsNotCorrect); 41 | return false; 42 | }; 43 | return true; 44 | } 45 | 46 | export async function doctor( 47 | log: Log = new ConsoleLog('doctor'), configPath?: string | undefined): Promise { 48 | const config = await loadOrCreateConfig(log, undefined, configPath); 49 | const jdkResult = await jdkDoctor(config, log); 50 | const androidSdkResult = await androidSdkDoctor(config, log); 51 | if (jdkResult && androidSdkResult) { 52 | log.info(messages.bothPathsAreValid); 53 | } 54 | return jdkResult && androidSdkResult; 55 | } 56 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/updateConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Config, Log, ConsoleLog} from '@bubblewrap/core'; 18 | import {ParsedArgs} from 'minimist'; 19 | import {existsSync} from 'fs'; 20 | import {loadOrCreateConfig} from '../config'; 21 | import {DEFAULT_CONFIG_FILE_PATH} from '../config'; 22 | import {enUS as messages} from '../strings'; 23 | 24 | async function updateAndroidSdkPath(path: string, log: Log): Promise { 25 | if (!existsSync(path)) { 26 | log.error('Please enter a valid path.'); 27 | return false; 28 | } 29 | const config = await loadOrCreateConfig(); 30 | const jdkPath = config.jdkPath; 31 | const newConfig = new Config(jdkPath, path); 32 | await newConfig.saveConfig(DEFAULT_CONFIG_FILE_PATH); 33 | return true; 34 | } 35 | 36 | async function updateJdkPath(path: string, log: Log): Promise { 37 | if (!existsSync(path)) { 38 | log.error('Please enter a valid path.'); 39 | return false; 40 | } 41 | const config = await loadOrCreateConfig(); 42 | const androidSdkPath = config.androidSdkPath; 43 | const newConfig = new Config(path, androidSdkPath); 44 | await newConfig.saveConfig(DEFAULT_CONFIG_FILE_PATH); 45 | return true; 46 | } 47 | 48 | export async function updateConfig(args: ParsedArgs, log: Log = new ConsoleLog('updateConfig')): 49 | Promise { 50 | if (args.jdkPath) { 51 | await updateJdkPath(args.jdkPath, log); 52 | } 53 | if (args.androidSdkPath) { 54 | await updateAndroidSdkPath(args.androidSdkPath, log); 55 | } 56 | if (!args.jdkPath && !args.androidSdkPath) { 57 | log.error(messages.updateConfigUsage); 58 | return false; 59 | } 60 | return true; 61 | } 62 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/java/LauncherActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package <%= packageId %>; 17 | 18 | import android.content.pm.ActivityInfo; 19 | import android.net.Uri; 20 | import android.os.Build; 21 | import android.os.Bundle; 22 | 23 | <% for(const imp of launcherActivity.imports) { %> 24 | import <%= imp %>; 25 | <% } %> 26 | 27 | public class LauncherActivity 28 | extends com.google.androidbrowserhelper.trusted.LauncherActivity { 29 | <% for(const variable of launcherActivity.variables) { %> 30 | <%= variable %> 31 | <% } %> 32 | 33 | <% for(const method of launcherActivity.methods) { %> 34 | <%= method %> 35 | <% } %> 36 | 37 | @Override 38 | protected void onCreate(Bundle savedInstanceState) { 39 | super.onCreate(savedInstanceState); 40 | // Setting an orientation crashes the app due to the transparent background on Android 8.0 41 | // Oreo and below. We only set the orientation on Oreo and above. This only affects the 42 | // splash screen and Chrome will still respect the orientation. 43 | // See https://github.com/GoogleChromeLabs/bubblewrap/issues/496 for details. 44 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { 45 | setRequestedOrientation(<%= toAndroidScreenOrientation(orientation) %>); 46 | } else { 47 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); 48 | } 49 | } 50 | 51 | @Override 52 | protected Uri getLaunchingUrl() { 53 | // Get the original launch Url. 54 | Uri uri = super.getLaunchingUrl(); 55 | 56 | <% for(const code of launcherActivity.launchUrl) { %> 57 | <%= code %> 58 | <% } %> 59 | 60 | return uri; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | 17 | # Bubblewrap Core 18 | ![Node CI Status](https://github.com/GoogleChromeLabs/bubblewrap/workflows/Node%20CI/badge.svg) 19 | 20 | Bubblewrap Core is a NodeJS library that helps developers to create a Project for an Android 21 | application that launches an existing Progressive Web App (PWA) using a 22 | [Trusted Web Activity (TWA)](https://developer.chrome.com/docs/android/trusted-web-activity/). 23 | 24 | ## Requirements 25 | - [Node.js](https://nodejs.org/en/) 14.15.0 or above 26 | 27 | ## Setting up the Environment 28 | 29 | ### Get the Java Development Kit (JDK) 8. 30 | The Android Command line tools requires the correct version of the JDK to run. To prevent version 31 | conflicts with a JDK version that is already installed, Bubblewrap uses a JDK that can unzipped in 32 | a separate folder. 33 | 34 | Download a version of JDK 8 that is compatible with your OS from 35 | [AdoptOpenJDK](https://adoptopenjdk.net/releases.html?variant=openjdk8&jvmVariant=hotspot) 36 | and extract it in its own folder. 37 | 38 | **Warning:** Using a version lower than 8 will make it impossible to compile the project and higher 39 | versions are incompatible with the Android command line tools. 40 | 41 | ### Get the Android command line tools 42 | Download a version of Android command line tools that is compatible with your OS from 43 | [https://developer.android.com/studio#command-tools](https://developer.android.com/studio#command-tools). 44 | Create a folder and extract the downloaded file into it. 45 | 46 | ## Using @bubblewrap/core in a NodeJs project 47 | 48 | ```shell 49 | npm i -g @bubblewrap/core 50 | ``` 51 | 52 | ## Contributing 53 | 54 | See [CONTRIBUTING](../../CONTRIBUTING.md) for more. 55 | 56 | ## License 57 | 58 | See [LICENSE](../../LICENSE) for more. 59 | 60 | ## Disclaimer 61 | 62 | This is not a Google product. 63 | 64 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/PlayBillingFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {EmptyFeature} from './EmptyFeature'; 18 | 19 | export type PlayBillingConfig = { 20 | enabled: boolean; 21 | } 22 | 23 | export class PlayBillingFeature extends EmptyFeature { 24 | constructor() { 25 | super('playbilling'); 26 | 27 | this.buildGradle.dependencies.push('com.google.androidbrowserhelper:billing:1.1.0'); 28 | 29 | this.androidManifest.components.push(` 30 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | `); 53 | 54 | this.delegationService.imports.push( 55 | 'com.google.androidbrowserhelper.playbilling.digitalgoods.DigitalGoodsRequestHandler'); 56 | this.delegationService.onCreate = 57 | 'registerExtraCommandHandler(new DigitalGoodsRequestHandler(getApplicationContext()));'; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/TwaGeneratorSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {TwaGenerator} from '../../lib/TwaGenerator'; 18 | import * as mockFs from 'mock-fs'; 19 | import {existsSync} from 'fs'; 20 | 21 | describe('TwaGenerator', () => { 22 | it('Builds an instance of TwaGenerator', () => { 23 | const twaGenerator = new TwaGenerator(); 24 | expect(twaGenerator).not.toBeNull(); 25 | }); 26 | 27 | describe('#removeTwaProject', () => { 28 | it('Removes project files', async () => { 29 | mockFs({ 30 | '/root': { 31 | 'app': { 32 | 'build.gradle': 'build.gradle content', 33 | 'src': { 34 | 'AndroidManifest.xml': 'Android Manifest', 35 | }, 36 | }, 37 | 'gradle': { 38 | 'gradlew.bat': 'gradlew content', 39 | }, 40 | 'settings.gradle': 'settings.gradle', 41 | 'gradle.properties': 'gradle.properties', 42 | 'build.gradle': 'build.gradle', 43 | 'gradlew': 'gradlew', 44 | 'gradlew.bat': 'gradle.bat', 45 | 'twa-manifest.json': 'twa-manifest.json', 46 | 'android.keystore': 'keystore', 47 | }, 48 | }); 49 | 50 | const twaGenerator = new TwaGenerator(); 51 | await twaGenerator.removeTwaProject('/root'); 52 | expect(existsSync('/root/app')).toBeFalse(); 53 | expect(existsSync('/root/gradle')).toBeFalse(); 54 | expect(existsSync('/root/settings.gradle')).toBeFalse(); 55 | expect(existsSync('/root/build.gradle')).toBeFalse(); 56 | expect(existsSync('/root/gradlew')).toBeFalse(); 57 | expect(existsSync('/root/gradlew.bat')).toBeFalse(); 58 | expect(existsSync('/root/twa-manifest.json')).toBeTrue(); 59 | expect(existsSync('/root/android.keystore')).toBeTrue(); 60 | mockFs.restore(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/core/src/lib/types/FileHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface FileHandlerJson { 18 | action?: string; 19 | accept?: { 20 | [mimeType: string]: Array; 21 | }; 22 | } 23 | 24 | export interface FileHandler { 25 | actionUrl: string; 26 | mimeTypes: Array; 27 | } 28 | 29 | function normalizeUrl(url: string, startUrl: URL, scopeUrl: URL): string | undefined { 30 | try { 31 | const absoluteUrl = new URL(url, startUrl); 32 | 33 | if (absoluteUrl.protocol !== 'https:') { 34 | console.warn('Ignoring url with illegal scheme:', absoluteUrl.toString()); 35 | return; 36 | } 37 | 38 | if (absoluteUrl.origin != scopeUrl.origin) { 39 | console.warn('Ignoring url with invalid origin:', absoluteUrl.toString()); 40 | return; 41 | } 42 | 43 | if (!absoluteUrl.pathname.startsWith(scopeUrl.pathname)) { 44 | console.warn('Ignoring url not within manifest scope: ', absoluteUrl.toString()); 45 | return; 46 | } 47 | 48 | return absoluteUrl.toString(); 49 | } catch (error) { 50 | console.warn('Ignoring invalid url:', url); 51 | } 52 | } 53 | 54 | export function processFileHandlers( 55 | fileHandlers: FileHandlerJson[], 56 | startUrl: URL, 57 | scopeUrl: URL, 58 | ): FileHandler[] { 59 | const processedFileHandlers: FileHandler[] = []; 60 | 61 | for (const handler of fileHandlers) { 62 | if (!handler.action || !handler.accept) continue; 63 | 64 | const actionUrl = normalizeUrl(handler.action, startUrl, scopeUrl); 65 | if (!actionUrl) continue; 66 | 67 | const mimeTypes = Object.keys(handler.accept); 68 | if (mimeTypes.length == 0) continue; 69 | 70 | const processedHandler: FileHandler = { 71 | actionUrl, 72 | mimeTypes, 73 | }; 74 | 75 | processedFileHandlers.push(processedHandler); 76 | } 77 | 78 | return processedFileHandlers; 79 | } 80 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/GradleWrapperSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {GradleWrapper} from '../../lib/GradleWrapper'; 18 | import {Config} from '../../lib/Config'; 19 | import {JdkHelper} from '../../lib/jdk/JdkHelper'; 20 | import {AndroidSdkTools} from '../../lib/androidSdk/AndroidSdkTools'; 21 | import * as util from '../../lib/util'; 22 | import * as fs from 'fs'; 23 | 24 | describe('GradleWrapper', () => { 25 | let gradleWrapper: GradleWrapper; 26 | let androidSdkTools: AndroidSdkTools; 27 | 28 | const cwd = '/path/to/twa-project/'; 29 | const process = { 30 | platform: 'linux', 31 | env: { 32 | 'PATH': '', 33 | }, 34 | cwd: () => cwd, 35 | } as unknown as NodeJS.Process; 36 | 37 | beforeEach(async () => { 38 | spyOn(fs, 'existsSync').and.returnValue(true); 39 | const config = new Config('/home/user/jdk8', '/home/user/sdktools'); 40 | const jdkHelper = new JdkHelper(process, config); 41 | androidSdkTools = await AndroidSdkTools.create(process, config, jdkHelper); 42 | gradleWrapper = new GradleWrapper(process, androidSdkTools); 43 | }); 44 | 45 | describe('#bundleRelease', () => { 46 | it('Calls "gradle bundleRelease --stacktrace"', async () => { 47 | spyOn(util, 'executeFile').and.stub(); 48 | await gradleWrapper.bundleRelease(); 49 | expect(util.executeFile).toHaveBeenCalledWith('./gradlew', 50 | ['bundleRelease', '--stacktrace'], androidSdkTools.getEnv(), undefined, cwd); 51 | }); 52 | }); 53 | 54 | describe('#assembleRelease', () => { 55 | it('Calls "gradle assembleRelease --stacktrace"', async () => { 56 | spyOn(util, 'executeFile').and.stub(); 57 | await gradleWrapper.assembleRelease(); 58 | expect(util.executeFile).toHaveBeenCalledWith('./gradlew', 59 | ['assembleRelease', '--stacktrace'], androidSdkTools.getEnv(), undefined, cwd); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/jdk/JarSignerSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {JarSigner} from '../../../lib/jdk/JarSigner'; 18 | import {JdkHelper} from '../../../lib/jdk/JdkHelper'; 19 | import {Config} from '../../../lib/Config'; 20 | import {SigningKeyInfo} from '../../../lib/TwaManifest'; 21 | import * as util from '../../../lib/util'; 22 | import * as fs from 'fs'; 23 | 24 | const CWD = '/path/to/twa-project/'; 25 | const PROCESS = { 26 | platform: 'linux', 27 | env: { 28 | 'PATH': '', 29 | }, 30 | cwd: () => CWD, 31 | } as unknown as NodeJS.Process; 32 | 33 | const SIGNING_KEY_INFO: SigningKeyInfo = { 34 | path: '/path/to/keystore/', 35 | alias: 'myalias', 36 | }; 37 | 38 | const STORE_PASS = 'mystorepass'; 39 | const KEY_PASS = 'mykeypass'; 40 | const INPUT_AAB = '/path/to/input.aab'; 41 | const OUTPUT_AAB = './output.aab'; 42 | 43 | describe('JarSigner', () => { 44 | describe('#sign', () => { 45 | it('Invokes the correct signing command', async () => { 46 | spyOn(fs, 'existsSync').and.returnValue(true); 47 | const config = new Config('/home/user/jdk8', '/home/user/sdktools'); 48 | const jdkHelper = new JdkHelper(PROCESS, config); 49 | const jarSigner = new JarSigner(jdkHelper); 50 | 51 | spyOn(util, 'executeFile').and.stub(); 52 | await jarSigner.sign(SIGNING_KEY_INFO, STORE_PASS, KEY_PASS, INPUT_AAB, OUTPUT_AAB); 53 | expect(util.executeFile).toHaveBeenCalledWith('jarsigner', [ 54 | '-verbose', 55 | '-sigalg', 56 | 'SHA256withRSA', 57 | '-digestalg', 58 | 'SHA-256', 59 | '-keystore', 60 | SIGNING_KEY_INFO.path, 61 | INPUT_AAB, 62 | SIGNING_KEY_INFO.alias, 63 | '-storepass', 64 | STORE_PASS, 65 | '-keypass', 66 | KEY_PASS, 67 | '-signedjar', 68 | OUTPUT_AAB, 69 | ], jdkHelper.getEnv()); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/ImageHelperSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import * as Jimp from 'jimp'; 19 | import Color = require('color'); 20 | import {ImageHelper} from '../../lib/ImageHelper'; 21 | 22 | function samePixels(actual: Jimp, expected: Jimp): void { 23 | const w = expected.getWidth(); 24 | const h = expected.getHeight(); 25 | expect(actual.getWidth()).toBe(w); 26 | expect(actual.getWidth()).toBe(h); 27 | 28 | for (const {x, y} of expected.scanIterator(0, 0, w, h)) { 29 | const actualPixel = Jimp.intToRGBA(actual.getPixelColor(x, y)); 30 | const expectedPixel = Jimp.intToRGBA(expected.getPixelColor(x, y)); 31 | 32 | if (expectedPixel.a === 0) { 33 | // Don't care about color of transparent pixel 34 | expect(actualPixel.a).toBe(0, `Pixel at ${x}, ${y}`); 35 | } else { 36 | expect(actualPixel).toEqual(expectedPixel, `Pixel at ${x}, ${y}`); 37 | } 38 | } 39 | } 40 | 41 | describe('ImageHelper', () => { 42 | describe('#constructor', () => { 43 | it('Builds an instance of ImageHelper', () => { 44 | const imageHelper = new ImageHelper(); 45 | expect(imageHelper).not.toBeNull(); 46 | }); 47 | }); 48 | 49 | describe('#monochromeFilter', () => { 50 | it('Changes the color of a monochrome icon', async () => { 51 | const fixturesDirectory = path.join(__dirname, '../../../src/spec/fixtures'); 52 | const data = await Jimp.read(path.join(fixturesDirectory, 'add_task.png')); 53 | const expected = await Jimp.read(path.join(fixturesDirectory, 'add_task_coloured.png')); 54 | 55 | const imageHelper = new ImageHelper(); 56 | const red = new Color('#ff0000'); 57 | const url = 'https://example.com/icon.png'; 58 | const coloured = await imageHelper.monochromeFilter({url, data}, red); 59 | 60 | expect(coloured.url).toEqual(url); 61 | samePixels(coloured.data, expected); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /packages/cli/src/lib/AndroidSdkToolsInstaller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import {util} from '@bubblewrap/core'; 19 | import {Prompt} from './Prompt'; 20 | import {enUS as messages} from './strings'; 21 | 22 | const SDK_VERSION = '6609375'; 23 | const DOWNLOAD_SDK_ROOT = 'https://dl.google.com/android/repository/'; 24 | const WINDOWS_URL = `commandlinetools-win-${SDK_VERSION}_latest.zip`; 25 | const MAC_URL = `commandlinetools-mac-${SDK_VERSION}_latest.zip`; 26 | const LINUX_URL = `commandlinetools-linux-${SDK_VERSION}_latest.zip`; 27 | 28 | /** 29 | * Install Android Command Line Tools by downloading the zip and 30 | * decompressing it. 31 | */ 32 | export class AndroidSdkToolsInstaller { 33 | constructor(private process: NodeJS.Process, private prompt: Prompt) { 34 | } 35 | 36 | /** 37 | * Downloads the platform-appropriate version of Android 38 | * Command Line Tools. 39 | * 40 | * @param installPath {string} path to install SDK at. 41 | */ 42 | async install(installPath: string): Promise { 43 | let downloadFileName; 44 | switch (this.process.platform) { 45 | case 'darwin': { 46 | downloadFileName = MAC_URL; 47 | break; 48 | } 49 | case 'linux': { 50 | downloadFileName = LINUX_URL; 51 | break; 52 | } 53 | case 'win32': { 54 | downloadFileName = WINDOWS_URL; 55 | break; 56 | } 57 | default: throw new Error(`Unsupported Platform: ${this.process.platform}`); 58 | } 59 | 60 | const dstPath = path.resolve(installPath); 61 | const downloadUrl = DOWNLOAD_SDK_ROOT + downloadFileName; 62 | const localPath = path.join(dstPath, downloadFileName); 63 | 64 | this.prompt.printMessage(messages.messageDownloadAndroidSdk); 65 | await this.prompt.downloadFile(downloadUrl, localPath); 66 | 67 | this.prompt.printMessage(messages.messageDecompressAndroidSdk); 68 | await util.unzipFile(localPath, dstPath, true); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/src/lib/GradleWrapper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {executeFile} from './util'; 18 | import {AndroidSdkTools} from './androidSdk/AndroidSdkTools'; 19 | 20 | /** 21 | * A Wrapper around the Gradle commands. 22 | */ 23 | export class GradleWrapper { 24 | private process: NodeJS.Process; 25 | private androidSdkTools: AndroidSdkTools; 26 | private projectLocation: string; 27 | private gradleCmd: string; 28 | 29 | /** 30 | * Builds a new GradleWrapper 31 | * @param {NodeJS.Process} process NodeJS process information. 32 | * @param {AndroidSdkTools} androidSdkTools Android SDK to be used when building a project. 33 | * @param {string} projectLocation The location of the Android project. 34 | */ 35 | constructor( 36 | process: NodeJS.Process, androidSdkTools: AndroidSdkTools, projectLocation?: string) { 37 | this.process = process; 38 | this.androidSdkTools = androidSdkTools; 39 | this.projectLocation = projectLocation || this.process.cwd(); 40 | 41 | if (process.platform === 'win32') { 42 | this.gradleCmd = 'gradlew.bat'; 43 | } else { 44 | this.gradleCmd = './gradlew'; 45 | } 46 | } 47 | 48 | /** 49 | * Invokes `gradle bundleRelease` for the Android project. 50 | */ 51 | async bundleRelease(): Promise { 52 | await this.executeGradleCommand(['bundleRelease', '--stacktrace']); 53 | } 54 | 55 | /** 56 | * Invokes `gradle assembleRelease` for the Android project. 57 | */ 58 | async assembleRelease(): Promise { 59 | await this.executeGradleCommand(['assembleRelease', '--stacktrace']); 60 | } 61 | 62 | /** 63 | * Executes gradle commands with custom arguments. 64 | * @param args - Arguments supplied to gradle, also considered gradle tasks. 65 | */ 66 | async executeGradleCommand(args: string[]): Promise { 67 | const env = this.androidSdkTools.getEnv(); 68 | await executeFile( 69 | this.gradleCmd, args, env, undefined, this.projectLocation); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/cli/src/spec/MockPromptSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Result} from '@bubblewrap/core'; 18 | import {MockPrompt} from './mock/MockPrompt'; 19 | 20 | async function validationFunction(message: string): Promise> { 21 | return Result.ok(message); 22 | } 23 | 24 | describe('MockPrompt', () => { 25 | describe('#promptInput', () => { 26 | it('Checks if the correct messages are being prompted using promptInput', async () => { 27 | const mock = new MockPrompt(); 28 | mock.addMessage('first'); 29 | mock.addMessage('second'); 30 | expect(await mock.promptInput('', null, validationFunction)).toBe('first'); 31 | expect(await mock.promptInput('', null, validationFunction)).toBe('second'); 32 | mock.addMessage('third'); 33 | expect(await mock.promptInput('', null, validationFunction)).toBe('third'); 34 | }); 35 | }); 36 | 37 | describe('#promptChoice', () => { 38 | it('Checks if the correct messages are being prompted using promptChoice', async () => { 39 | const mock = new MockPrompt(); 40 | mock.addMessage('first'); 41 | mock.addMessage('second'); 42 | expect(await mock.promptChoice('', [], null, validationFunction)).toBe('first'); 43 | expect(await mock.promptChoice('', [], null, validationFunction)).toBe('second'); 44 | mock.addMessage('third'); 45 | expect(await mock.promptChoice('', [], null, validationFunction)).toBe('third'); 46 | }); 47 | }); 48 | 49 | describe('#promptPassword', () => { 50 | it('Checks if the correct messages are being prompted using promptPassword', async () => { 51 | const mock = new MockPrompt(); 52 | mock.addMessage('first'); 53 | mock.addMessage('second'); 54 | expect(await mock.promptPassword('', validationFunction)).toBe('first'); 55 | expect(await mock.promptPassword('', validationFunction)).toBe('second'); 56 | mock.addMessage('third'); 57 | expect(await mock.promptPassword('', validationFunction)).toBe('third'); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/core/src/lib/types/WebManifest.ts: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 2019 Google Inc. All Rights Reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import {FileHandlerJson} from './FileHandler'; 19 | import {ProtocolHandler} from './ProtocolHandler'; 20 | 21 | export interface WebManifestIcon { 22 | src: string; 23 | sizes?: string; 24 | purpose?: string; 25 | mimeType?: string; 26 | size?: number; 27 | } 28 | 29 | export interface WebManifestShortcutJson { 30 | name?: string; 31 | short_name?: string; 32 | url?: string; 33 | icons?: Array; 34 | } 35 | 36 | type WebManifestDisplayMode = 'browser' | 'minimal-ui' | 'standalone' | 'fullscreen'; 37 | 38 | export type WebManifestDisplayOverrideValue = 'window-controls-overlay' | 'tabbed' | 'browser' | 39 | 'minimal-ui' | 'standalone' | 'fullscreen'; 40 | 41 | export type OrientationLock = 'any' | 'natural' | 'landscape'| 'portrait' | 'portrait-primary'| 42 | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary'; 43 | 44 | // These interfaces follows the implementation from: https://w3c.github.io/web-share-target/. 45 | export interface ShareTargetParams { 46 | title?: string; 47 | text?: string; 48 | url?: string; 49 | files?: FilesParams[]; 50 | } 51 | 52 | export interface FilesParams { 53 | name: string; 54 | accept: string[]; 55 | } 56 | 57 | export interface ShareTarget { 58 | action?: string; 59 | method?: string; 60 | enctype?: string; 61 | params?: ShareTargetParams; 62 | } 63 | 64 | export interface WebManifestJson { 65 | name?: string; 66 | short_name?: string; 67 | start_url?: string; 68 | scope?: string; 69 | display?: WebManifestDisplayMode; 70 | display_override?: WebManifestDisplayOverrideValue[]; 71 | theme_color?: string; 72 | background_color?: string; 73 | icons?: Array; 74 | shortcuts?: Array; 75 | share_target?: ShareTarget; 76 | orientation?: OrientationLock; 77 | protocol_handlers?: Array; 78 | file_handlers?: Array; 79 | launch_handler?: { 80 | client_mode?: string; 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/DigitalAssetLinksSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {DigitalAssetLinks} from '../../lib/DigitalAssetLinks'; 18 | 19 | const packageName = 'com.test.twa'; 20 | 21 | describe('DigitalAssetLinks', () => { 22 | describe('#generateAssetLinks', () => { 23 | it('Generates the assetlinks markup', () => { 24 | const fingerprint = 'FINGERPRINT'; 25 | const digitalAssetLinks = 26 | JSON.parse(DigitalAssetLinks.generateAssetLinks(packageName, fingerprint)); 27 | expect(digitalAssetLinks.length).toBe(1); 28 | expect(digitalAssetLinks[0].relation.length).toBe(1); 29 | expect(digitalAssetLinks[0].relation[0]).toBe('delegate_permission/common.handle_all_urls'); 30 | expect(digitalAssetLinks[0].target.namespace).toBe('android_app'); 31 | expect(digitalAssetLinks[0].target.package_name).toBe(packageName); 32 | expect(digitalAssetLinks[0].target.sha256_cert_fingerprints.length).toBe(1); 33 | expect(digitalAssetLinks[0].target.sha256_cert_fingerprints[0]).toBe(fingerprint); 34 | }); 35 | 36 | it('Generates empty assetlinks.json', () => { 37 | const digitalAssetLinks = 38 | JSON.parse(DigitalAssetLinks.generateAssetLinks(packageName, ...new Array())); 39 | expect(digitalAssetLinks.length).toBe(0); 40 | }); 41 | 42 | it('Supports multiple fingerprints', () => { 43 | const digitalAssetLinks = 44 | JSON.parse(DigitalAssetLinks.generateAssetLinks(packageName, '123', '456')); 45 | expect(digitalAssetLinks.length).toBe(1); 46 | expect(digitalAssetLinks[0].relation[0]).toBe('delegate_permission/common.handle_all_urls'); 47 | expect(digitalAssetLinks[0].target.namespace).toBe('android_app'); 48 | expect(digitalAssetLinks[0].target.package_name).toBe(packageName); 49 | expect(digitalAssetLinks[0].target.sha256_cert_fingerprints.length).toBe(2); 50 | expect(digitalAssetLinks[0].target.sha256_cert_fingerprints[0]).toBe('123'); 51 | expect(digitalAssetLinks[0].target.sha256_cert_fingerprints[1]).toBe('456'); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/core/template_project/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | <% if (additionalTrustedOrigins.length > 0) { %> 19 | 20 | <% for (const additionalOrigin of additionalTrustedOrigins) { %> 21 | https://<%= additionalOrigin %> 22 | <% } %> 23 | 24 | <% } %> 25 | 26 | 30 | <% if (displayOverride.length > 0) { %> 31 | 32 | <% for (const displayOverrideValue of displayOverride) { %> 33 | <% if (displayOverrideValue === 'fullscreen') { %> 34 | immersive 35 | <% } else if (displayOverrideValue === 'fullscreen-sticky') { %> 36 | sticky-immersive 37 | <% } else { %> 38 | <%= displayOverrideValue %> 39 | <% } %> 40 | <% } %> 41 | 42 | <% } %> 43 | 44 | 50 | 51 | [{ 52 | \"relation\": [\"delegate_permission/common.handle_all_urls\"], 53 | \"target\": { 54 | \"namespace\": \"web\", 55 | \"site\": \"https://<%= host %>\" 56 | } 57 | }] 58 | <% for (const additionalOrigin of additionalTrustedOrigins) { %> 59 | ,[{ 60 | \"relation\": [\"delegate_permission/common.handle_all_urls\"], 61 | \"target\": { 62 | \"namespace\": \"web\", 63 | \"site\": \"https://<%= additionalOrigin %>\" 64 | } 65 | }] 66 | <% } %> 67 | 68 | 69 | -------------------------------------------------------------------------------- /packages/core/src/lib/BufferedLog.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Log} from './Log'; 18 | 19 | enum LogLevel { 20 | Debug, 21 | Info, 22 | Warn, 23 | Error, 24 | } 25 | 26 | /** Data class to store level and message of pending */ 27 | class PendingLog { 28 | constructor(public level: LogLevel, public message: string) {} 29 | } 30 | 31 | /** 32 | * A Log that wraps another Log, saving up all the log calls to be applied when flush is called. 33 | * 34 | * It doesn't currently support arguments to the log messages. 35 | */ 36 | export class BufferedLog implements Log { 37 | private pendingLogs: Array = []; 38 | 39 | constructor(private innerLog: Log) {} 40 | 41 | // The "no-invalid-this" triggers incorrectly on the below code, see: 42 | // https://github.com/eslint/eslint/issues/13894 43 | // https://github.com/typescript-eslint/typescript-eslint/issues/2840 44 | /* eslint-disable no-invalid-this */ 45 | debug = this.addLogFunction(LogLevel.Debug); 46 | info = this.addLogFunction(LogLevel.Info); 47 | warn = this.addLogFunction(LogLevel.Warn); 48 | error = this.addLogFunction(LogLevel.Error); 49 | /* eslint-enable no-invalid-this */ 50 | 51 | /** Creates a function that adds a log at the given level. */ 52 | private addLogFunction(level: LogLevel): (message: string) => void { 53 | return (message: string): void => { 54 | this.pendingLogs.push(new PendingLog(level, message)); 55 | }; 56 | } 57 | 58 | setVerbose(verbose: boolean): void { 59 | this.innerLog.setVerbose(verbose); 60 | } 61 | 62 | /** Flushes all recorded logs to the underlying object. */ 63 | flush(): void { 64 | this.pendingLogs.forEach((pendingLog) => { 65 | const message = pendingLog.message; 66 | 67 | switch (pendingLog.level) { 68 | case LogLevel.Debug: 69 | this.innerLog.debug(message); 70 | break; 71 | case LogLevel.Info: 72 | this.innerLog.info(message); 73 | break; 74 | case LogLevel.Warn: 75 | this.innerLog.warn(message); 76 | break; 77 | case LogLevel.Error: 78 | this.innerLog.error(message); 79 | break; 80 | } 81 | }); 82 | 83 | this.pendingLogs = []; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/ResultSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Result} from '../../lib/Result'; 18 | 19 | describe('Result', () => { 20 | describe('#ok()', () => { 21 | it('creates an `ok` result', () => { 22 | const message = 'I am ok'; 23 | const result: Result = Result.ok(message); 24 | expect(result.isOk()).toBeTrue(); 25 | expect(result.isError()).toBeFalse(); 26 | expect(result.unwrap()).toEqual(message); 27 | }); 28 | }); 29 | 30 | describe('#error()', () => { 31 | it('creates an `error` result', () => { 32 | const errorMessage = 'This might be an error...'; 33 | const error = new Error(errorMessage); 34 | const result: Result = Result.error(error); 35 | expect(result.isOk()).toBeFalse(); 36 | expect(result.isError()).toBeTrue(); 37 | expect(result.unwrap).toThrowError(); 38 | }); 39 | }); 40 | 41 | describe('#unwrapOr()', () => { 42 | it('returns value when result is `ok`', () => { 43 | const message = 'I am ok'; 44 | const defaultMessage = 'I am not ok'; 45 | const result: Result = Result.ok(message); 46 | expect(result.isOk()).toBeTrue(); 47 | expect(result.isError()).toBeFalse(); 48 | expect(result.unwrapOr(defaultMessage)).toEqual(message); 49 | }); 50 | 51 | it('returns default value when result is `error`', () => { 52 | const defaultMessage = 'I am not ok'; 53 | const result: Result = Result.error(new Error('oopsy')); 54 | expect(result.isOk()).toBeFalse(); 55 | expect(result.isError()).toBeTrue(); 56 | expect(result.unwrapOr(defaultMessage)).toEqual(defaultMessage); 57 | }); 58 | }); 59 | 60 | describe('#unwrapError()', () => { 61 | it('returns the Error when result is `error`', () => { 62 | const error = new Error('I am not ok'); 63 | const result: Result = Result.error(error); 64 | expect(result.isOk()).toBeFalse(); 65 | expect(result.isError()).toBeTrue(); 66 | expect(result.unwrapError()).toEqual(error); 67 | }); 68 | 69 | it('throws exception when result is `ok`', () => { 70 | const result: Result = Result.ok('I have bad feeling about this...'); 71 | expect(result.unwrapError).toThrowError(); 72 | }); 73 | }); 74 | }); 75 | 76 | -------------------------------------------------------------------------------- /packages/validator/src/lib/psi/PsiResult.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export type LighthouseCategoryName = 18 | 'accessibility' | 'best-practices' | 'performance' | 'pwa' | 'seo'; 19 | export type LighthouseEmulatedFormFactor = 'desktop' | 'mobile'; 20 | 21 | export type LighthouseCategory = { 22 | id: LighthouseCategoryName; 23 | title: string; 24 | description: string; 25 | manualDescription: string; 26 | score: number; 27 | } 28 | 29 | export type LighthouseCategories = { 30 | [key in LighthouseCategoryName]: LighthouseCategory; 31 | }; 32 | 33 | export type LighthouseMetricAudit = { 34 | firstContentfulPaint: number; 35 | largestContentfulPaint: number; 36 | maxPotentialFID: number; 37 | cumulativeLayoutShift: number; 38 | } 39 | 40 | export type LighthouseMetricsAudit = { 41 | id: string; 42 | title: string; 43 | description: string; 44 | details: { 45 | type: string; 46 | // The API returns an array, but are only interested in the first item. 47 | items: LighthouseMetricAudit[]; 48 | }; 49 | }; 50 | 51 | export type PsiLighthouseResult = { 52 | requestedUrl: string; 53 | finalUrl: string; 54 | lighthouseVersion: string; 55 | userAgent: string; 56 | fetchTime: string; 57 | environment: { 58 | networkUserAgent: string; 59 | hostUserAgent: string; 60 | benchmarkIndex: number; 61 | }; 62 | configSettings: { 63 | emulatedFormFactor: LighthouseEmulatedFormFactor; 64 | locale: string; 65 | onlyCategories: LighthouseCategoryName[]; 66 | channel: string; 67 | }; 68 | audits: { 69 | metrics: LighthouseMetricsAudit; 70 | }; 71 | categories: LighthouseCategories; 72 | timing: { 73 | total: number; 74 | }; 75 | }; 76 | 77 | /** 78 | * Defines the types from the PageSpeed Insights API response, relevant to the Trusted Web Activity 79 | * validation. A comprehensive documentation on fields available for the API response is available 80 | * at https://developers.google.com/speed/docs/insights/v5/reference/pagespeedapi/runpagespeed#response_1. 81 | */ 82 | export type PsiResult = { 83 | captchaResult: string; 84 | kind: string; 85 | id: string; 86 | loadingExperience: { 87 | initial_url: string; 88 | }; 89 | lighthouseResult: PsiLighthouseResult; 90 | analysisUTCTimestamp: string; 91 | version: { 92 | major: number; 93 | minor: number; 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 17 | # Bubblewrap 18 | [![Node CI Status](https://github.com/GoogleChromeLabs/bubblewrap/workflows/Node%20CI/badge.svg)](https://github.com/GoogleChromeLabs/bubblewrap/actions?query=workflow%3A%22Node+CI%22) 19 | 20 | Bubblewrap is a set of tools and libraries designed to help developers to create, build and update 21 | projects for Android Applications that launch Progressive Web App (PWA) using 22 | [Trusted Web Activity (TWA)](https://developer.chrome.com/docs/android/trusted-web-activity/). 23 | 24 | ## Requirements 25 | - [Node.js](https://nodejs.org/en/) 14.15.0 or above 26 | 27 | ## Getting Started 28 | - To get started with building an application using Bubblewrap, check the [Trusted Web Activity 29 | Quick Start Guide][1] or the [bubblewrap/cli](./packages/cli) documentation. 30 | 31 | ## Bubblewrap Components 32 | 33 | - **[bubblewrap/core](./packages/core):** a javascript library for generating, building and 34 | updating TWA projects. 35 | - **[bubblewrap/cli](./packages/cli):** a command-line version of Bubblewrap. 36 | - **[bubblewrap/validator](./packages/validator):** library to validate the correctness and 37 | compare Trusted Web Activity projects against the quality criteria. 38 | 39 | ## Community 40 | 41 | We welcome anyone who wants to contribute with issues, feedback, feature requests or just 42 | generally discuss Bubblewrap. Alternatively developers can contribute to the conversation 43 | by joining the public monthly office hours, which hosted on every first Thursday at 5PM, 44 | London time. Check when the next office hours is going to happen via [this calendar][5] 45 | and join the meeting via [this link][3]. 46 | 47 | ## Getting started with GUI tools 48 | - If you are just getting started with APK generation from PWA, You might want to check [PWABuilder](https://www.pwabuilder.com/). 49 | This tool is powered by Bubblewrap and uses the same underlying core. 50 | 51 | ## Contributing 52 | 53 | See [CONTRIBUTING](./CONTRIBUTING.md) for more. 54 | 55 | ## License 56 | 57 | See [LICENSE](./LICENSE) for more. 58 | 59 | ## Disclaimer 60 | 61 | This is not an officially supported Google product. 62 | 63 | [1]: https://developer.chrome.com/docs/android/trusted-web-activity/quick-start/ 64 | [2]: https://join.slack.com/t/chromiumdev/shared_invite/zt-4b4af0yu-1mZ7uF6pCjYMC4poRr8Bkg 65 | [3]: https://meet.google.com/hps-wjke-qac 66 | [4]: https://chromiumdev.slack.com/archives/C01829L0URJ 67 | [5]: https://calendar.google.com/calendar/embed?src=c_jovg5osnfku7kigbo7joh1reug%40group.calendar.google.com&ctz=Europe%2FLondon&mode=AGENDA 68 | [6]: https://chromiumdev.slack.com/ 69 | -------------------------------------------------------------------------------- /packages/core/template_project/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Google Open Source Community Guidelines 2 | 3 | At Google, we recognize and celebrate the creativity and collaboration of open 4 | source contributors and the diversity of skills, experiences, cultures, and 5 | opinions they bring to the projects and communities they participate in. 6 | 7 | Every one of Google's open source projects and communities are inclusive 8 | environments, based on treating all individuals respectfully, regardless of 9 | gender identity and expression, sexual orientation, disabilities, 10 | neurodiversity, physical appearance, body size, ethnicity, nationality, race, 11 | age, religion, or similar personal characteristic. 12 | 13 | We value diverse opinions, but we value respectful behavior more. 14 | 15 | Respectful behavior includes: 16 | 17 | * Being considerate, kind, constructive, and helpful. 18 | * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or 19 | physically threatening behavior, speech, and imagery. 20 | * Not engaging in unwanted physical contact. 21 | 22 | Some Google open source projects [may adopt][] an explicit project code of 23 | conduct, which may have additional detailed expectations for participants. Most 24 | of those projects will use our [modified Contributor Covenant][]. 25 | 26 | [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct 27 | [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ 28 | 29 | ## Resolve peacefully 30 | 31 | We do not believe that all conflict is necessarily bad; healthy debate and 32 | disagreement often yields positive results. However, it is never okay to be 33 | disrespectful. 34 | 35 | If you see someone behaving disrespectfully, you are encouraged to address the 36 | behavior directly with those involved. Many issues can be resolved quickly and 37 | easily, and this gives people more control over the outcome of their dispute. 38 | If you are unable to resolve the matter for any reason, or if the behavior is 39 | threatening or harassing, report it. We are dedicated to providing an 40 | environment where participants feel welcome and safe. 41 | 42 | ## Reporting problems 43 | 44 | Some Google open source projects may adopt a project-specific code of conduct. 45 | In those cases, a Google employee will be identified as the Project Steward, 46 | who will receive and handle reports of code of conduct violations. In the event 47 | that a project hasn’t identified a Project Steward, you can report problems by 48 | emailing opensource@google.com. 49 | 50 | We will investigate every complaint, but you may not receive a direct response. 51 | We will use our discretion in determining when and how to follow up on reported 52 | incidents, which may range from not taking action to permanent expulsion from 53 | the project and project-sponsored spaces. We will notify the accused of the 54 | report and provide them an opportunity to discuss it before any action is 55 | taken. The identity of the reporter will be omitted from the details of the 56 | report supplied to the accused. In potentially harmful situations, such as 57 | ongoing harassment or threats to anyone's safety, we may take action without 58 | notice. 59 | 60 | *This document was adapted from the [IndieWeb Code of Conduct][] and can also 61 | be found at .* 62 | 63 | [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct 64 | -------------------------------------------------------------------------------- /packages/cli/command_flow.dot: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * If you change this file you'll have to manually update the image, to do so 19 | * you must install GraphViz and run the following command: 20 | * 21 | * dot -Tsvg command_flow.dot -o command_flow.svg 22 | */ 23 | digraph bubble { 24 | rankdir="LR"; 25 | 26 | subgraph processes { 27 | node[shape=box, style=filled, fontcolor=white, fillcolor=black]; 28 | 29 | init; 30 | build; 31 | update; 32 | merge; 33 | validate; 34 | install; 35 | doctor; 36 | updateConfig; 37 | } 38 | 39 | subgraph artifacts { 40 | web_app_manifest[label="web app manifest"]; 41 | updated_web_app_manifest[label="updated web app manifest"]; 42 | twa_manifest[label="twaManifest.json"]; 43 | config[label="~/bubblewrap/config.json"]; 44 | project[label="Android project"]; 45 | apk[label="APK / AAB"]; 46 | installed[label="Installed APK"]; 47 | assetlinks[label="assetlinks.json"]; 48 | } 49 | 50 | // Init takes the web app manifest, generates the config, twa manifest and 51 | // Android project. 52 | web_app_manifest -> init; 53 | init -> config; 54 | init -> twa_manifest; 55 | init -> project; 56 | 57 | // UpdateConf modifies the conf and doctor checks it. 58 | updateConfig -> config; 59 | config -> doctor; 60 | 61 | // Update takes the manifest and applies it to the project. 62 | twa_manifest -> update; 63 | update -> project; 64 | 65 | // Build turns the project into an APK and generates the asset links. 66 | project -> build; 67 | build -> apk; 68 | build -> assetlinks; 69 | 70 | // Install installs the Android project. 71 | apk -> install; 72 | install -> installed; 73 | 74 | // Merge takes a new web app manifest and applies to the twa manifest. 75 | updated_web_app_manifest -> merge; 76 | twa_manifest -> merge; 77 | merge -> twa_manifest; 78 | 79 | // Validate just looks at the website. 80 | web_app_manifest -> validate; 81 | 82 | // Most of the commands depend on the config. 83 | subgraph config_deps { 84 | edge[style=dotted]; 85 | 86 | // config -> build; 87 | // config -> install; 88 | } 89 | 90 | // Some constraints to lay the graph out nicely. 91 | { rank = same; web_app_manifest; validate; } 92 | { rank = same; init; merge; updateConfig; } 93 | { rank = same; config; doctor; twa_manifest; } 94 | { rank = same; update; project; build; } 95 | { rank = same; assetlinks; apk; install; installed; } 96 | 97 | // Some invisible edges to lay the graph out nicely. 98 | subgraph layout { 99 | edge[style=invisible, arrowhead=none]; 100 | 101 | project -> assetlinks; 102 | assetlinks -> apk; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/AppsFlyerFeature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {EmptyFeature} from './EmptyFeature'; 18 | 19 | export type AppsFlyerConfig = { 20 | enabled: boolean; 21 | appsFlyerId: string; 22 | } 23 | 24 | export class AppsFlyerFeature extends EmptyFeature { 25 | constructor(config: AppsFlyerConfig) { 26 | super('appsFlyer'); 27 | // Setup build.gradle. 28 | this.buildGradle.repositories.push('mavenCentral()'); 29 | this.buildGradle.dependencies.push('com.appsflyer:af-android-sdk:5.4.0'); 30 | 31 | // Setup the Android Manifest. 32 | this.androidManifest.permissions.push( 33 | 'android.permission.INTERNET', 34 | 'android.permission.ACCESS_NETWORK_STATE', 35 | 'android.permission.ACCESS_WIFI_STATE', 36 | // TODO(andreban): this may be optional. Check and remove if that's confirmed. 37 | 'android.permission.READ_PHONE_STATE'); 38 | this.androidManifest.components.push( 39 | ` 42 | 43 | 44 | 45 | `); 46 | 47 | // Setup the Application class. 48 | this.applicationClass.imports.push( 49 | 'java.util.Map', 50 | 'com.appsflyer.AppsFlyerLib', 51 | 'com.appsflyer.AppsFlyerConversionListener'); 52 | this.applicationClass.variables.push( 53 | `private static final String AF_DEV_KEY = "${config.appsFlyerId}";`); 54 | this.applicationClass.onCreate = 55 | `AppsFlyerConversionListener conversionListener = new AppsFlyerConversionListener() { 56 | @Override 57 | public void onConversionDataSuccess(Map conversionData) { 58 | } 59 | 60 | @Override 61 | public void onConversionDataFail(String errorMessage) { 62 | } 63 | 64 | @Override 65 | public void onAppOpenAttribution(Map attributionData) { 66 | } 67 | 68 | @Override 69 | public void onAttributionFailure(String errorMessage) { 70 | } 71 | }; 72 | AppsFlyerLib.getInstance().init(AF_DEV_KEY, conversionListener, this); 73 | AppsFlyerLib.getInstance().startTracking(this);`; 74 | 75 | // Setup the LauncherActivity. 76 | this.launcherActivity.imports.push('com.appsflyer.AppsFlyerLib'); 77 | this.launcherActivity.launchUrl = 78 | `String appsFlyerId = AppsFlyerLib.getInstance().getAppsFlyerUID(this); 79 | uri = uri 80 | .buildUpon() 81 | .appendQueryParameter("appsflyer_id", appsFlyerId) 82 | .build();`; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/cli/src/spec/DoctorSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import mock from 'mock-fs'; 18 | import {MockLog} from '@bubblewrap/core'; 19 | import {doctor} from '../lib/cmds/doctor'; 20 | import {enUS as messages} from '../lib/strings'; 21 | 22 | describe('doctor', () => { 23 | describe('#jdkDoctor', () => { 24 | it('checks that the expected error message is sent in case that the path given isn\'t' + 25 | ' valid', async () => { 26 | // Creates a mock file system. 27 | mock({ 28 | 'path/to/sdk': { 29 | 'tools': {}, 30 | }, 31 | 'path/to/config': '{"jdkPath":"path/to/jdk","androidSdkPath":"path/to/sdk"}', 32 | }); 33 | 34 | const mockLog = new MockLog(); 35 | await expectAsync(doctor(mockLog, 'path/to/config')).toBeResolvedTo(false); 36 | // Check that the correct message was sent. 37 | const logErrors: Array = mockLog.getReceivedData(); 38 | const lastMessage = logErrors[logErrors.length - 1]; 39 | expect(lastMessage.indexOf('jdkPath isn\'t correct')).toBeGreaterThanOrEqual(0); 40 | mock.restore(); 41 | }); 42 | 43 | xit('checks that the expected error message is sent in case that the jdk isn\'t' + 44 | ' supported', async () => { 45 | // Creates a mock file system. 46 | mock({ 47 | 'path/to/jdk': { 48 | 'release': 'JAVA_VERSION="1.8', 49 | }, 50 | 'path/to/sdk': { 51 | 'tools': {}, 52 | }, 53 | 'path/to/config': '{"jdkPath":"path/to/jdk","androidSdkPath":"path/to/sdk"}', 54 | }); 55 | 56 | const mockLog = new MockLog(); 57 | await expectAsync(doctor(mockLog, 'path/to/config')).toBeResolvedTo(false); 58 | // Check that the correct message was sent. 59 | const logErrors: Array = mockLog.getReceivedData(); 60 | const lastMessage = logErrors[logErrors.length - 1]; 61 | expect(lastMessage.indexOf('Unsupported jdk version')).toBeGreaterThanOrEqual(0); 62 | mock.restore(); 63 | }); 64 | }); 65 | 66 | describe('#androidSdkDoctor', () => { 67 | it('checks that the expected error message is sent in case that the path given isn\'t' + 68 | ' valid', async () => { 69 | // Creates a mock file system. 70 | mock({ 71 | 'path/to/jdk': { 72 | 'release': 'JAVA_VERSION="1.8', 73 | }, 74 | 'path/to/config': '{"jdkPath":"path/to/jdk","androidSdkPath":"path/to/sdk"}', 75 | }); 76 | 77 | const mockLog = new MockLog(); 78 | 79 | await expectAsync(doctor(mockLog, 'path/to/config')).toBeResolvedTo(false); 80 | 81 | // Check that the correct message was sent. 82 | const logErrors: Array = mockLog.getReceivedData(); 83 | expect(logErrors[logErrors.length - 1]).toEqual(messages.androidSdkPathIsNotCorrect); 84 | mock.restore(); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/core/src/lib/Result.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | type ValueType = { 18 | type: 'ok'; 19 | value: V; 20 | }; 21 | 22 | type ErrorType = { 23 | type: 'error'; 24 | error: E; 25 | } 26 | 27 | type ResultType = ValueType | ErrorType; 28 | 29 | /** 30 | * While throwing exceptions show be used to handle truly exception cases, `Result` can be 31 | * used to handle cases where failures are expected, therefore not really exceptions. 32 | * 33 | * The outcome can be verified with `Result.isOk()`: 34 | * ``` 35 | * const result = someMethod(); 36 | * if (result.isOk()) { 37 | * const value = result.unwrap(); 38 | * ... 39 | * } 40 | * ``` 41 | */ 42 | export class Result { 43 | private _result: ResultType; 44 | 45 | private constructor(result: ResultType) { 46 | this._result = result; 47 | } 48 | 49 | /** 50 | * Creates a new `ok` Result, with the outcome `value`. 51 | */ 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | static ok(value: V): Result { 54 | return new Result({ 55 | type: 'ok', 56 | value: value, 57 | }); 58 | } 59 | 60 | /** 61 | * Creates a new `error` Result, with the outcome `error`. 62 | */ 63 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 64 | static error(error: E): Result { 65 | return new Result({ 66 | type: 'error', 67 | error: error, 68 | }); 69 | } 70 | 71 | /** 72 | * Returns the value if this result is `ok`. Otherwise, throws `E`. 73 | */ 74 | unwrap(): V { 75 | if (this._result.type === 'error') { 76 | throw this._result.error; 77 | } 78 | return this._result.value; 79 | } 80 | 81 | /** 82 | * If the result is an Error, returns `defaultValue`. Otherwise returns the result value. 83 | */ 84 | unwrapOr(defaultValue: V): V { 85 | if (this._result.type === 'error') { 86 | return defaultValue; 87 | } 88 | return this._result.value; 89 | } 90 | 91 | /** 92 | * If the result is an Error, returns the `Error` instance without throwing. Otherwise, 93 | * throws an Exception. 94 | */ 95 | unwrapError(): E { 96 | if (this._result.type === 'error') { 97 | return this._result.error; 98 | } 99 | throw new Error('Expected result to be "ok", but it is "error"'); 100 | } 101 | 102 | /** 103 | * @returns `true` if the result is `ok`. `false` if it is an `Error`. 104 | */ 105 | isOk(): boolean { 106 | return this._result.type === 'ok'; 107 | } 108 | 109 | /** 110 | * @returns `true` if the result is an `Error`. `false` if the result is `ok`. 111 | */ 112 | isError(): boolean { 113 | return this._result.type === 'error'; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/FileHandlerSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | processFileHandlers, 19 | FileHandler, 20 | FileHandlerJson, 21 | } from '../../lib/types/FileHandler'; 22 | 23 | describe('FileHandler', () => { 24 | describe('#processFileHandlers', () => { 25 | it('Accepts valid file handlers', () => { 26 | const startUrl = new URL('https://test.com/app/start'); 27 | const scopeUrl = new URL('https://test.com/app'); 28 | const testHandlers: FileHandlerJson[] = [ 29 | { 30 | 'action': '/app', 31 | 'accept': { 32 | 'text/plain': ['.txt'], 33 | }, 34 | }, 35 | { 36 | 'action': '/app?image', 37 | 'accept': { 38 | 'image/jpeg': ['.jpg', 'jpeg'], 39 | }, 40 | }, 41 | ]; 42 | const expectedHandlers: FileHandler[] = [ 43 | { 44 | actionUrl: 'https://test.com/app', 45 | mimeTypes: ['text/plain'], 46 | }, 47 | { 48 | actionUrl: 'https://test.com/app?image', 49 | mimeTypes: ['image/jpeg'], 50 | }, 51 | ]; 52 | const processedHandlers = processFileHandlers(testHandlers, startUrl, scopeUrl); 53 | expect(processedHandlers).toEqual(expectedHandlers); 54 | }); 55 | it('Rejects invalid file handlers', () => { 56 | const startUrl = new URL('https://test.com/app/start'); 57 | const scopeUrl = new URL('https://test.com/app'); 58 | const testHandlers: FileHandlerJson[] = [ 59 | { 60 | 'action': '/app', 61 | }, 62 | { 63 | 'accept': { 64 | 'text/plain': ['.txt'], 65 | }, 66 | }, 67 | { 68 | 'action': '/app?image', 69 | 'accept': {}, 70 | }, 71 | ]; 72 | const processedHandlers = processFileHandlers(testHandlers, startUrl, scopeUrl); 73 | expect(processedHandlers).toEqual([]); 74 | }); 75 | it('Rejects invalid action URLs', () => { 76 | const startUrl = new URL('https://test.com/app/start'); 77 | const scopeUrl = new URL('https://test.com/app'); 78 | const testHandlers: FileHandlerJson[] = [ 79 | { 80 | 'action': '/', // not withing the scope 81 | 'accept': { 82 | 'text/plain': ['.txt'], 83 | }, 84 | }, 85 | { 86 | 'action': 'http://test.com/app', // invalid protocol 87 | 'accept': { 88 | 'text/plain': ['.txt'], 89 | }, 90 | }, 91 | { 92 | 'action': 'http://a.test.com/app', // invalid origin 93 | 'accept': { 94 | 'text/plain': ['.txt'], 95 | }, 96 | }, 97 | ]; 98 | const processedHandlers = processFileHandlers(testHandlers, startUrl, scopeUrl); 99 | expect(processedHandlers).toEqual([]); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /packages/core/src/lib/FetchUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as fs from 'fs'; 18 | import {context, Response as FetchH2Response} from 'fetch-h2'; 19 | import nodefetch, {Response as NodeFetchResponse} from 'node-fetch'; 20 | 21 | export type FetchEngine = 'node-fetch' | 'fetch-h2'; 22 | const DEFAULT_FETCH_ENGINE: FetchEngine = 'fetch-h2'; 23 | 24 | export type NodeFetchOrFetchH2Response = FetchH2Response | NodeFetchResponse; 25 | 26 | const userAgent = 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0'; 27 | const fetchh2Ctx = context({userAgent: userAgent, overwriteUserAgent: true}); 28 | const fetchh2 = fetchh2Ctx.fetch; 29 | 30 | class FetchUtils { 31 | private fetchEngine = DEFAULT_FETCH_ENGINE; 32 | 33 | setFetchEngine(newFetchEngine: FetchEngine): void { 34 | this.fetchEngine = newFetchEngine; 35 | } 36 | 37 | async fetch(input: string): Promise { 38 | if (this.fetchEngine == 'node-fetch') { 39 | return await nodefetch(input, {redirect: 'follow', headers: {'User-Agent': userAgent}}); 40 | } else { 41 | return await fetchh2(input, {redirect: 'follow'}); 42 | } 43 | } 44 | 45 | /** 46 | * Downloads a file from `url` and saves it to `path`. If a `progressCallback` function is passed, it 47 | * will be invoked for every chunk received. If the value of `total` parameter is -1, it means we 48 | * were unable to determine to total file size before starting the download. 49 | */ 50 | async downloadFile( 51 | url: string, 52 | path: string, 53 | progressCallback?: (current: number, total: number) => void, 54 | ): Promise { 55 | let result; 56 | let readableStream: NodeJS.ReadableStream; 57 | 58 | if (this.fetchEngine === 'node-fetch') { 59 | result = await nodefetch(url); 60 | readableStream = result.body; 61 | } else { 62 | result = await fetchh2(url, {redirect: 'follow'}); 63 | readableStream = await result.readable(); 64 | } 65 | 66 | // Try to determine the file size via the `Content-Length` header. This may not be available 67 | // for all cases. 68 | const contentLength = result.headers.get('content-length'); 69 | const fileSize = contentLength ? parseInt(contentLength) : -1; 70 | 71 | const fileStream = fs.createWriteStream(path); 72 | let received = 0; 73 | 74 | await new Promise((resolve, reject) => { 75 | readableStream.pipe(fileStream); 76 | 77 | // Even though we're piping the chunks, we intercept them to check for the download progress. 78 | if (progressCallback) { 79 | readableStream.on('data', (chunk) => { 80 | received = received + chunk.length; 81 | progressCallback(received, fileSize); 82 | }); 83 | } 84 | 85 | readableStream.on('error', (err) => { 86 | reject(err); 87 | }); 88 | 89 | fileStream.on('finish', () => { 90 | resolve({}); 91 | }); 92 | }); 93 | } 94 | } 95 | 96 | const fetchUtils = new FetchUtils(); 97 | export {fetchUtils}; 98 | -------------------------------------------------------------------------------- /packages/validator/src/lib/psi/PageSpeedInsights.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import fetch from 'node-fetch'; 18 | import {PsiResult} from './PsiResult'; 19 | 20 | const BASE_PSI_URL = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'; 21 | export type PsiStrategy = 'desktop' | 'mobile'; 22 | export type PsiCategory = 'accessibility' | 'best-practices' | 'performance' | 'pwa' | 'seo'; 23 | 24 | /** 25 | * A wrapper for a request to the PageSpeed Ingights API. 26 | */ 27 | export class PsiRequest { 28 | /** 29 | * Builds a new PsiRequest; 30 | * @param url the full URL of the PSI endpoint, with parameters 31 | */ 32 | constructor(readonly url: URL) { 33 | } 34 | } 35 | 36 | /** 37 | * Builds requests for the PSI endpoint. A full list of parameters is available at 38 | * https://developers.google.com/speed/docs/insights/v5/reference/pagespeedapi/runpagespeed 39 | */ 40 | export class PsiRequestBuilder { 41 | private url: URL; 42 | 43 | /** 44 | * Constructs a new PsiRequestBuilder instance 45 | * @param validationUrl the URL to be validated 46 | */ 47 | constructor(validationUrl: URL) { 48 | this.url = new URL(BASE_PSI_URL); 49 | this.setUrl(validationUrl); 50 | } 51 | 52 | /** 53 | * Sets the URL to be validated 54 | * @param url the URL to be validated 55 | */ 56 | setUrl(url: URL): PsiRequestBuilder { 57 | this.url.searchParams.delete('url'); 58 | this.url.searchParams.append('url', url.toString()); 59 | return this; 60 | } 61 | 62 | /** 63 | * Sets the strategy to use when validating a PWA. 64 | * @param {PsiStrategy} strategy 65 | */ 66 | setStrategy(strategy: PsiStrategy): PsiRequestBuilder { 67 | this.url.searchParams.delete('strategy'); 68 | this.url.searchParams.append('strategy', strategy); 69 | return this; 70 | } 71 | 72 | /** 73 | * Adds a category to be added when generating the PSI report. 74 | * @param {PsiCategory} category 75 | */ 76 | addCategory(category: PsiCategory): PsiRequestBuilder { 77 | this.url.searchParams.append('category', category); 78 | return this; 79 | } 80 | 81 | /** 82 | * Builds a PsiRequest using the parameters in this builder. 83 | * @returns {PsiRequest} 84 | */ 85 | build(): PsiRequest { 86 | return new PsiRequest(this.url); 87 | } 88 | } 89 | 90 | /** 91 | * A Wrapper for the PageSpeedInsights API. 92 | * 93 | * More information on the API is available at: 94 | * - https://developers.google.com/speed/docs/insights/v5/get-started 95 | */ 96 | export class PageSpeedInsights { 97 | async runPageSpeedInsights(request: PsiRequest): Promise { 98 | const response = await fetch(request.url); 99 | if (response.status !== 200) { 100 | throw new Error( 101 | `Failed to run the PageSpeed Insight report for ${request.url}. ` + 102 | `Server responded with status ${response.status}`); 103 | } 104 | return (await response.json()) as PsiResult; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/cli/src/lib/Cli.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import minimist from 'minimist'; 18 | import {update} from './cmds/update'; 19 | import {help} from './cmds/help'; 20 | import {build} from './cmds/build'; 21 | import {init, InitArgs} from './cmds/init'; 22 | import {validate} from './cmds/validate'; 23 | import {install} from './cmds/install'; 24 | import {loadOrCreateConfig} from './config'; 25 | import {major} from 'semver'; 26 | import {version} from './cmds/version'; 27 | import {BUBBLEWRAP_LOGO} from './constants'; 28 | import {updateConfig} from './cmds/updateConfig'; 29 | import {doctor} from './cmds/doctor'; 30 | import {merge} from './cmds/merge'; 31 | import {fingerprint} from './cmds/fingerprint'; 32 | import {fetchUtils} from '@bubblewrap/core'; 33 | import {play, PlayArgs} from './cmds/play'; 34 | 35 | export class Cli { 36 | async run(args: string[]): Promise { 37 | console.log(BUBBLEWRAP_LOGO); 38 | if (major(process.versions.node) < 14) { 39 | throw new Error(`Current Node.js version is ${process.versions.node}.` + 40 | ' Node.js version 14 or above is required to run Bubblewrap.'); 41 | } 42 | const parsedArgs = minimist(args); 43 | if (parsedArgs.fetchEngine && 44 | (parsedArgs.fetchEngine == 'node-fetch' || parsedArgs.fetchEngine == 'fetch-h2')) { 45 | fetchUtils.setFetchEngine(parsedArgs.fetchEngine); 46 | } 47 | 48 | const config = await loadOrCreateConfig(undefined, undefined, parsedArgs.config); 49 | 50 | let command; 51 | if (parsedArgs._.length === 0) { 52 | // Accept --version and --help alternatives for the help and version commands. 53 | if (parsedArgs.version) { 54 | command = 'version'; 55 | } else if (parsedArgs.help) { 56 | command = 'help'; 57 | } 58 | } else { 59 | command = parsedArgs._[0]; 60 | } 61 | 62 | // If no command is given, default to 'help'. 63 | if (!command) { 64 | command = 'help'; 65 | } 66 | 67 | switch (command) { 68 | case 'help': 69 | return await help(parsedArgs); 70 | case 'init': 71 | return await init(parsedArgs as unknown as InitArgs, config); 72 | case 'update': 73 | return await update(parsedArgs); 74 | case 'build': 75 | return await build(config, parsedArgs); 76 | case 'validate': 77 | return await validate(parsedArgs); 78 | case 'version': { 79 | return await version(); 80 | } 81 | case 'install': 82 | return await install(parsedArgs, config); 83 | case 'updateConfig': 84 | return await updateConfig(parsedArgs); 85 | case 'doctor': 86 | return await doctor(); 87 | case 'merge': 88 | return await merge(parsedArgs); 89 | case 'fingerprint': 90 | return await fingerprint(parsedArgs); 91 | case 'play': 92 | return await play(parsedArgs as unknown as PlayArgs); 93 | default: 94 | throw new Error( 95 | `"${command}" is not a valid command! Use 'bubblewrap help' for a list of commands`); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/core/src/lib/types/ProtocolHandler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export interface ProtocolHandler { 18 | protocol: string; 19 | url: string; 20 | } 21 | 22 | const ProtocolHandlerExtraScheme = /^web\+[a-z]+$/; 23 | const ProtcolHandlerAllowedSchemes = [ 24 | 'bitcoin', 'ftp', 'ftps', 'geo', 'im', 'irc', 'ircs', 25 | 'magnet', 'mailto', 'matrix', 'news', 'nntp', 'openpgp4fpr', 26 | 'sftp', 'sip', 'ssh', 'urn', 'webcal', 'wtai', 'xmpp', 27 | ]; 28 | // 'mms', 'sms', 'smsto', and 'tel' are not supported! 29 | 30 | function normalizeProtocol(protocol: string): string | undefined { 31 | const normalized = protocol.toLowerCase(); 32 | 33 | if (ProtcolHandlerAllowedSchemes.includes(normalized)) { 34 | return normalized; 35 | } 36 | 37 | if (ProtocolHandlerExtraScheme.test(normalized)) { 38 | return normalized; 39 | } 40 | 41 | console.warn('Ignoring invalid protocol:', protocol); 42 | return undefined; 43 | } 44 | 45 | function normalizeUrl(url: string, startUrl: URL, scopeUrl: URL): string | undefined { 46 | if (!url.includes('%s')) { 47 | console.warn('Ignoring url without %%s:', url); 48 | return undefined; 49 | } 50 | 51 | try { 52 | const absoluteUrl = new URL(url); 53 | 54 | if (absoluteUrl.protocol !== 'https:') { 55 | console.warn('Ignoring absolute url with illegal scheme:', absoluteUrl.toString()); 56 | return undefined; 57 | } 58 | 59 | if (absoluteUrl.origin != scopeUrl.origin) { 60 | console.warn('Ignoring absolute url with invalid origin:', absoluteUrl.toString()); 61 | return undefined; 62 | } 63 | 64 | if (!absoluteUrl.pathname.startsWith(scopeUrl.pathname)) { 65 | console.warn('Ignoring absolute url not within manifest scope: ', absoluteUrl.toString()); 66 | return undefined; 67 | } 68 | 69 | return absoluteUrl.toString(); 70 | } catch (error) { 71 | // Expected, url might be relative! 72 | } 73 | 74 | try { 75 | const relativeUrl = new URL(url, startUrl); 76 | return relativeUrl.toString(); 77 | } catch (error) { 78 | console.warn('Ignoring invalid relative url:', url); 79 | } 80 | } 81 | 82 | export function processProtocolHandlers( 83 | protocolHandlers: ProtocolHandler[], 84 | startUrl: URL, 85 | scopeUrl: URL, 86 | ): ProtocolHandler[] { 87 | const processedProtocolHandlers: ProtocolHandler[] = []; 88 | 89 | for (const handler of protocolHandlers) { 90 | if (!handler.protocol || !handler.url) continue; 91 | 92 | const normalizedProtocol = normalizeProtocol(handler.protocol); 93 | const normalizedUrl = normalizeUrl(handler.url, startUrl, scopeUrl); 94 | 95 | if (!normalizedProtocol || !normalizedUrl) { 96 | continue; 97 | } 98 | 99 | processedProtocolHandlers.push({protocol: normalizedProtocol, url: normalizedUrl}); 100 | } 101 | 102 | return processedProtocolHandlers; 103 | } 104 | 105 | export function normalizeProtocolForTesting(protocol: string): string | undefined { 106 | return normalizeProtocol(protocol); 107 | } 108 | 109 | export function normalizeUrlForTesting( 110 | url: string, 111 | startUrl: URL, 112 | scopeUrl: URL, 113 | ): string | undefined { 114 | return normalizeUrl(url, startUrl, scopeUrl); 115 | } 116 | -------------------------------------------------------------------------------- /packages/core/src/spec/lib/ShortcutInfoSpec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {ShortcutInfo} from '../../lib/ShortcutInfo'; 18 | 19 | describe('ShortcutInfo', () => { 20 | describe('#fromShortcutJson', () => { 21 | it('creates a correct TWA shortcut', () => { 22 | const shortcut = { 23 | 'name': 'shortcut name', 24 | 'short_name': 'short', 25 | 'url': '/launch', 26 | 'icons': [{ 27 | 'src': '/shortcut_icon.png', 28 | 'sizes': '96x96', 29 | }], 30 | }; 31 | const manifestUrl = new URL('https://pwa-directory.com/manifest.json'); 32 | const shortcutInfo = ShortcutInfo.fromShortcutJson(manifestUrl, shortcut); 33 | expect(shortcutInfo.name).toBe('shortcut name'); 34 | expect(shortcutInfo.shortName).toBe('short'); 35 | expect(shortcutInfo.url).toBe('https://pwa-directory.com/launch'); 36 | expect(shortcutInfo.chosenIconUrl) 37 | .toBe('https://pwa-directory.com/shortcut_icon.png'); 38 | expect(shortcutInfo.toString(0)) 39 | .toBe('[name:\'shortcut name\', short_name:\'short\',' + 40 | ' url:\'https://pwa-directory.com/launch\', icon:\'shortcut_0\']'); 41 | }); 42 | 43 | it('Throws if icon size is empty or too small', () => { 44 | const shortcut = { 45 | 'name': 'invalid', 46 | 'url': '/invalid', 47 | 'icons': [{ 48 | 'src': '/no_size.png', 49 | }, { 50 | 'src': '/small_size.png', 51 | 'sizes': '95x95', 52 | }], 53 | }; 54 | const manifestUrl = new URL('https://pwa-directory.com/manifest.json'); 55 | expect(() => ShortcutInfo.fromShortcutJson(manifestUrl, shortcut)) 56 | .toThrowError('not finding a suitable icon'); 57 | }); 58 | 59 | it('Throws if there is no any or monochrome icon', () => { 60 | const shortcut = { 61 | 'name': 'invalid', 62 | 'url': '/invalid', 63 | 'icons': [{ 64 | 'src': '/shortcut_icon.png', 65 | 'sizes': '96x96', 66 | 'purpose': 'maskable', 67 | }], 68 | }; 69 | const manifestUrl = new URL('https://pwa-directory.com/manifest.json'); 70 | expect(() => ShortcutInfo.fromShortcutJson(manifestUrl, shortcut)) 71 | .toThrowError('not finding a suitable icon'); 72 | }); 73 | 74 | it('Throws if icons is missing', () => { 75 | const shortcut = { 76 | 'name': 'invalid', 77 | 'url': '/invalid', 78 | }; 79 | const manifestUrl = new URL('https://pwa-directory.com/manifest.json'); 80 | expect(() => ShortcutInfo.fromShortcutJson(manifestUrl, shortcut)) 81 | .toThrowError('missing metadata'); 82 | }); 83 | }); 84 | 85 | describe('#constructor', () => { 86 | it('Builds a ShortcutInfo correctly', () => { 87 | const shortcutInfo = new ShortcutInfo('name', 'shortName', '/', 'icon.png'); 88 | expect(shortcutInfo.name).toEqual('name'); 89 | expect(shortcutInfo.shortName).toEqual('shortName'); 90 | expect(shortcutInfo.url).toEqual('/'); 91 | expect(shortcutInfo.chosenIconUrl).toEqual('icon.png'); 92 | }); 93 | 94 | it('Throws if chosenIconUrl is undefined', () => { 95 | expect(() => new ShortcutInfo('name', 'shortName', '/')).toThrow(); 96 | }); 97 | 98 | it('Throws if chosenMonochromeIconUrl is undefined', () => { 99 | expect(() => 100 | new ShortcutInfo('name', 'shortName', '/', undefined, 'maskable.png', undefined), 101 | ).toThrow(); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /packages/core/src/lib/ShortcutInfo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | import {findSuitableIcon, escapeGradleString} from './util'; 20 | import {WebManifestShortcutJson, WebManifestIcon} from './types/WebManifest'; 21 | 22 | // As described on https://developer.chrome.com/apps/manifest/name#short_name 23 | const SHORT_NAME_MAX_SIZE = 12; 24 | 25 | // The minimum size needed for the shortcut icon 26 | const MIN_SHORTCUT_ICON_SIZE = 96; 27 | 28 | /** 29 | * A wrapper around the WebManifest's ShortcutInfo. 30 | */ 31 | export class ShortcutInfo { 32 | /** 33 | * @param {string} name 34 | * @param {string} shortName 35 | * @param {string} url target Url for when the shortcut is clicked 36 | * @param {string} chosenIconUrl Url for the icon with an "any" purpose 37 | * @param {string} chosenMaskableIconUrl Url for the icon with a maskable purpose 38 | * @param {string} chosenMonochromeIconUrl Url for the icon with a monochrome purpose 39 | */ 40 | constructor(readonly name: string, readonly shortName: string, readonly url: string, 41 | readonly chosenIconUrl?: string, readonly chosenMaskableIconUrl?: string, 42 | readonly chosenMonochromeIconUrl?: string) { 43 | if (!chosenIconUrl && !chosenMonochromeIconUrl) { 44 | throw new Error( 45 | `ShortcutInfo ${name} must have either chosenIconUrl or chosenMonochromeIconUrl`); 46 | } 47 | } 48 | 49 | toString(index: number): string { 50 | return `[name:'${escapeGradleString(this.name)}', ` + 51 | `short_name:'${escapeGradleString(this.shortName)}', ` + 52 | `url:'${this.url}', ` + 53 | `icon:'${this.assetName(index)}']`; 54 | } 55 | 56 | assetName(index: number): string { 57 | return `shortcut_${index}`; 58 | } 59 | 60 | /** 61 | * Creates a new TwaManifest, using the URL for the Manifest as a base URL and uses the content 62 | * of the Web Manifest to generate the fields for the TWA Manifest. 63 | * 64 | * @param {URL} webManifestUrl the URL where the webmanifest is available. 65 | * @param {WebManifest} webManifest the Web Manifest, used as a base for the TWA Manifest. 66 | * @returns {TwaManifest} 67 | */ 68 | static fromShortcutJson(webManifestUrl: URL, shortcut: WebManifestShortcutJson): ShortcutInfo { 69 | const name = shortcut.name || shortcut.short_name; 70 | 71 | if (!shortcut.icons || !shortcut.url || !name) { 72 | throw new Error('missing metadata'); 73 | } 74 | 75 | const suitableIcon = findSuitableIcon(shortcut.icons, 'any', MIN_SHORTCUT_ICON_SIZE); 76 | const suitableMaskableIcon = 77 | findSuitableIcon(shortcut.icons, 'maskable', MIN_SHORTCUT_ICON_SIZE); 78 | const suitableMonochromeIcon = 79 | findSuitableIcon(shortcut.icons, 'monochrome', MIN_SHORTCUT_ICON_SIZE); 80 | 81 | if (!suitableIcon && !suitableMonochromeIcon) { 82 | // maskable icons also need an equivalent any icon for lower API versions. 83 | // any and monochrome icons work on all API versions. 84 | throw new Error('not finding a suitable icon'); 85 | } 86 | 87 | function resolveIconUrl(icon: WebManifestIcon | null): string | undefined { 88 | return icon ? new URL(icon.src, webManifestUrl).toString() : undefined; 89 | } 90 | 91 | const shortName = shortcut.short_name || shortcut.name!.substring(0, SHORT_NAME_MAX_SIZE); 92 | const url = new URL(shortcut.url, webManifestUrl).toString(); 93 | const shortcutInfo = new ShortcutInfo(name!, shortName!, url, resolveIconUrl(suitableIcon), 94 | resolveIconUrl(suitableMaskableIcon), resolveIconUrl(suitableMonochromeIcon)); 95 | 96 | return shortcutInfo; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/cli/src/spec/mock/MockPrompt.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {Prompt, ValidateFunction} from '../../lib/Prompt'; 18 | 19 | /** 20 | * A class which used for testing and which mocks user's input. 21 | */ 22 | export class MockPrompt implements Prompt { 23 | private responses: string[] = []; 24 | 25 | /** 26 | * Sets the next answer of this class to be the given message. 27 | * @param message the message to be returned in the next prompt message. 28 | */ 29 | addMessage(message: string): void { 30 | this.responses.push(message); 31 | } 32 | 33 | async printMessage(): Promise { 34 | // An empty function for testing. 35 | } 36 | 37 | /** 38 | * Sets the output to be the given message. 39 | * @param message the message to be prompt. Not relevant for tests. 40 | * @param {string | null} defaultValue a default value or null. 41 | * @param {ValidateFunction} validateFunction a function to validate the input. 42 | * @returns {Promise} a {@link Promise} that resolves to the validated loaded message, 43 | * converted to `T` by the `validateFunction`. 44 | */ 45 | async promptInput(_message: string, 46 | _defaultValue: string | null, 47 | validateFunction: ValidateFunction): Promise { 48 | const nextResponse = this.getNextMessage(); 49 | return (await validateFunction(nextResponse)).unwrap(); 50 | } 51 | 52 | /** 53 | * Sets the output to be the given message. 54 | * @param message the message to be prompt. Not relevant for tests. 55 | * @param {string[]} choices a list of choices. Not relevant for testing. 56 | * @param {string | null} defaultValue a default value or null. 57 | * @param {ValidateFunction} validateFunction a function to validate the input. 58 | * @returns {Promise} a {@link Promise} that resolves to the validated loaded message, 59 | * converted to `T` by the `validateFunction`. 60 | */ 61 | async promptChoice(_message: string, 62 | _choices: string[], 63 | _defaultValue: string | null, 64 | validateFunction: ValidateFunction): Promise { 65 | const nextResponse = this.getNextMessage(); 66 | return (await validateFunction(nextResponse)).unwrap(); 67 | } 68 | 69 | /** 70 | * Sets the output to be the given message. 71 | * @param message the message to be prompt. Not relevant for tests. 72 | * @param defaultValue the value to be returned 73 | * @returns {Promise} a {@link Promise} that resolves to a {@link boolean} value. The 74 | * value will the `true` if the user answers `Yes` and `false` for `No`. 75 | */ 76 | async promptConfirm(_message: string, defaultValue: boolean): Promise { 77 | const nextResponse = this.getNextMessage(); 78 | if (nextResponse === 'true') { 79 | return true; 80 | } 81 | if (nextResponse === 'false') { 82 | return false; 83 | } 84 | return defaultValue; 85 | } 86 | 87 | /** 88 | * Sets the output to be the given message. 89 | * @param message the message to be prompt. Not relevant for tests. 90 | * @param {ValidateFunction} validateFunction a function to validate the input. 91 | * @returns {Promise} a {@link Promise} that resolves to the user input validated by 92 | * `validateFunction`. 93 | */ 94 | async promptPassword(_message: string, validateFunction: ValidateFunction, 95 | ): Promise { 96 | const nextResponse = this.getNextMessage(); 97 | return (await validateFunction(nextResponse)).unwrap(); 98 | } 99 | 100 | /** 101 | * Sets the output to be the given message. 102 | * @param {ValidateFunction} validateFunction a function to validate the input. 103 | * @returns {string} which is the next message to be prompted`. 104 | */ 105 | private getNextMessage(): string { 106 | if (this.responses.length === 0) { 107 | throw new Error('No answer was given. Please use addMessage(nextMessage) before' + 108 | ' using this function'); 109 | } 110 | const nextResponse = this.responses.shift()!; 111 | return nextResponse; 112 | } 113 | 114 | async downloadFile(): Promise { 115 | throw new Error('Not Implemented'); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/validator/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bubblewrap/validator", 3 | "version": "1.22.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@bubblewrap/validator", 9 | "version": "1.22.0", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "@types/node-fetch": "^2.5.7", 13 | "node-fetch": "^2.6.0" 14 | }, 15 | "engines": { 16 | "node": ">=14.15.0" 17 | } 18 | }, 19 | "node_modules/@types/node": { 20 | "version": "14.14.28", 21 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz", 22 | "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==" 23 | }, 24 | "node_modules/@types/node-fetch": { 25 | "version": "2.5.8", 26 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz", 27 | "integrity": "sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==", 28 | "dependencies": { 29 | "@types/node": "*", 30 | "form-data": "^3.0.0" 31 | } 32 | }, 33 | "node_modules/asynckit": { 34 | "version": "0.4.0", 35 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 36 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 37 | }, 38 | "node_modules/combined-stream": { 39 | "version": "1.0.8", 40 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 41 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 42 | "dependencies": { 43 | "delayed-stream": "~1.0.0" 44 | }, 45 | "engines": { 46 | "node": ">= 0.8" 47 | } 48 | }, 49 | "node_modules/delayed-stream": { 50 | "version": "1.0.0", 51 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 52 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 53 | "engines": { 54 | "node": ">=0.4.0" 55 | } 56 | }, 57 | "node_modules/form-data": { 58 | "version": "3.0.1", 59 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", 60 | "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", 61 | "dependencies": { 62 | "asynckit": "^0.4.0", 63 | "combined-stream": "^1.0.8", 64 | "mime-types": "^2.1.12" 65 | }, 66 | "engines": { 67 | "node": ">= 6" 68 | } 69 | }, 70 | "node_modules/mime-db": { 71 | "version": "1.45.0", 72 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", 73 | "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", 74 | "engines": { 75 | "node": ">= 0.6" 76 | } 77 | }, 78 | "node_modules/mime-types": { 79 | "version": "2.1.28", 80 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", 81 | "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", 82 | "dependencies": { 83 | "mime-db": "1.45.0" 84 | }, 85 | "engines": { 86 | "node": ">= 0.6" 87 | } 88 | }, 89 | "node_modules/node-fetch": { 90 | "version": "2.6.7", 91 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 92 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 93 | "dependencies": { 94 | "whatwg-url": "^5.0.0" 95 | }, 96 | "engines": { 97 | "node": "4.x || >=6.0.0" 98 | }, 99 | "peerDependencies": { 100 | "encoding": "^0.1.0" 101 | }, 102 | "peerDependenciesMeta": { 103 | "encoding": { 104 | "optional": true 105 | } 106 | } 107 | }, 108 | "node_modules/tr46": { 109 | "version": "0.0.3", 110 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 111 | "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" 112 | }, 113 | "node_modules/webidl-conversions": { 114 | "version": "3.0.1", 115 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 116 | "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" 117 | }, 118 | "node_modules/whatwg-url": { 119 | "version": "5.0.0", 120 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 121 | "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", 122 | "dependencies": { 123 | "tr46": "~0.0.3", 124 | "webidl-conversions": "^3.0.0" 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /packages/cli/src/lib/JdkInstaller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import {util} from '@bubblewrap/core'; 19 | import {Prompt} from './Prompt'; 20 | import {enUS as messages} from './strings'; 21 | 22 | const JDK_VERSION = '17.0.11+9'; 23 | const JDK_DIR = `jdk-${JDK_VERSION}`; 24 | const DOWNLOAD_JDK_BIN_ROOT = `https://github.com/adoptium/temurin17-binaries/releases/download/jdk-${JDK_VERSION}/`; 25 | const DOWNLOAD_JDK_SRC_ROOT = 'https://github.com/adoptium/jdk17u/archive/refs/tags/'; 26 | const JDK_BIN_VERSION = JDK_VERSION.replace('+', '_'); 27 | const JDK_FILE_NAME_MAC_INTEL = `OpenJDK17U-jdk_x64_mac_hotspot_${JDK_BIN_VERSION}.tar.gz`; 28 | const JDK_FILE_NAME_MAC_APPLE = `OpenJDK17U-jdk_aarch64_mac_hotspot_${JDK_BIN_VERSION}.tar.gz`; 29 | const JDK_FILE_NAME_WIN32 = `OpenJDK17U-jdk_x86-32_windows_hotspot_${JDK_BIN_VERSION}.zip`; 30 | const JDK_FILE_NAME_LINUX64 = `OpenJDK17U-jdk_x64_linux_hotspot_${JDK_BIN_VERSION}.tar.gz`; 31 | const JDK_SRC_ZIP = `jdk-${JDK_VERSION}.zip`; 32 | const JDK_SOURCE_SIZE = 178978050; 33 | 34 | /** 35 | * Install JDK 17 by downloading the binary and source code and 36 | * decompressing it. Source code is required 37 | * based on discussions with legal team about licensing. 38 | */ 39 | export class JdkInstaller { 40 | private process: NodeJS.Process; 41 | private downloadFile: string; 42 | private unzipFunction: (srcPath: string, dstPath: string, deleteWhenDone: boolean) 43 | => Promise; 44 | private joinPath: (...paths: string[]) => string; 45 | 46 | /** 47 | * Constructs a new instance of JdkInstaller. 48 | * 49 | * @param process {NodeJS.Process} process information from the OS process. 50 | */ 51 | constructor(process: NodeJS.Process, private prompt: Prompt) { 52 | this.process = process; 53 | this.unzipFunction = util.untar; 54 | this.joinPath = path.posix.join; 55 | switch (process.platform) { 56 | case 'win32': { 57 | this.downloadFile = JDK_FILE_NAME_WIN32; 58 | this.unzipFunction = util.unzipFile; 59 | this.joinPath = path.win32.join; 60 | break; 61 | } 62 | case 'darwin': { 63 | switch (process.arch) { 64 | case 'x64': { 65 | this.downloadFile = JDK_FILE_NAME_MAC_INTEL; 66 | break; 67 | } 68 | case 'arm64': { 69 | this.downloadFile = JDK_FILE_NAME_MAC_APPLE; 70 | break; 71 | } 72 | default: 73 | this.downloadFile = ''; 74 | throw new Error(`Mac architecture unsupported: ${this.process.arch}`); 75 | } 76 | break; 77 | } 78 | case 'linux': { 79 | this.downloadFile = JDK_FILE_NAME_LINUX64; 80 | break; 81 | } 82 | default: 83 | this.downloadFile = ''; 84 | throw new Error(`Platform not found or unsupported: ${this.process.platform}.`); 85 | } 86 | } 87 | 88 | /** 89 | * Downloads the platform-appropriate version of JDK 17, including 90 | * binary and source code. 91 | * 92 | * @param installPath {string} path to install JDK at. 93 | */ 94 | async install(installPath: string): Promise { 95 | const dstPath = path.resolve(installPath); 96 | const downloadSrcUrl = DOWNLOAD_JDK_SRC_ROOT + JDK_SRC_ZIP; 97 | const localSrcZipPath = this.joinPath(dstPath, JDK_SRC_ZIP); 98 | 99 | this.prompt.printMessage(messages.messageDownloadJdkSrc); 100 | 101 | // The sources don't return the file size in the headers, so we 102 | // set it statically. 103 | await this.prompt.downloadFile(downloadSrcUrl, localSrcZipPath, JDK_SOURCE_SIZE); 104 | 105 | this.prompt.printMessage(messages.messageDecompressJdkSrc); 106 | await util.unzipFile(localSrcZipPath, dstPath, true); 107 | 108 | const downloadBinUrl = DOWNLOAD_JDK_BIN_ROOT + this.downloadFile; 109 | const localBinPath = this.joinPath(dstPath, this.downloadFile); 110 | 111 | this.prompt.printMessage(messages.messageDownloadJdkBin); 112 | await this.prompt.downloadFile(downloadBinUrl, localBinPath); 113 | 114 | this.prompt.printMessage(messages.messageDecompressJdkBin); 115 | await this.unzipFunction(localBinPath, dstPath, true); 116 | 117 | return this.joinPath(dstPath, JDK_DIR); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/cli/src/lib/cmds/fingerprint.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import type {ParsedArgs} from 'minimist'; 18 | import {TwaManifest, DigitalAssetLinks, Fingerprint} from '@bubblewrap/core'; 19 | import {TWA_MANIFEST_FILE_NAME, ASSETLINKS_OUTPUT_FILE} from '../constants'; 20 | import {Prompt, InquirerPrompt} from '../Prompt'; 21 | import * as path from 'path'; 22 | import * as fs from 'fs'; 23 | import {enUS} from '../strings'; 24 | import {validateSha256Fingerprint} from '../inputHelpers'; 25 | 26 | async function loadManifest(args: ParsedArgs, prompt: Prompt): Promise { 27 | const manifestFile = args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME); 28 | prompt.printMessage(enUS.messageLoadingTwaManifestFrom(manifestFile)); 29 | if (!fs.existsSync(manifestFile)) { 30 | throw new Error(enUS.errorCouldNotfindTwaManifest(manifestFile)); 31 | } 32 | return await TwaManifest.fromFile(manifestFile); 33 | } 34 | 35 | async function saveManifest( 36 | args: ParsedArgs, twaManifest: TwaManifest, prompt: Prompt): Promise { 37 | const manifestFile = args.manifest || path.join(process.cwd(), TWA_MANIFEST_FILE_NAME); 38 | prompt.printMessage(enUS.messageSavingTwaManifestTo(manifestFile)); 39 | await twaManifest.saveToFile(manifestFile); 40 | } 41 | 42 | async function generateAssetLinks( 43 | args: ParsedArgs, prompt: Prompt, twaManifest?: TwaManifest): Promise { 44 | twaManifest = twaManifest || await loadManifest(args, prompt); 45 | const fingerprints = twaManifest.fingerprints.map((value) => value.value); 46 | const digitalAssetLinks = 47 | DigitalAssetLinks.generateAssetLinks(twaManifest.packageId, ...fingerprints); 48 | const digitalAssetLinksFile = args.output || path.join(process.cwd(), ASSETLINKS_OUTPUT_FILE); 49 | await fs.promises.writeFile(digitalAssetLinksFile, digitalAssetLinks); 50 | prompt.printMessage(enUS.messageGeneratedAssetLinksFile(digitalAssetLinksFile)); 51 | return true; 52 | } 53 | 54 | async function addFingerprint(args: ParsedArgs, prompt: Prompt): Promise { 55 | if (args._.length < 3) { 56 | throw new Error(enUS.errorMissingArgument(3, args._.length)); 57 | } 58 | const fingerprintValue = (await validateSha256Fingerprint(args._[2])).unwrap(); 59 | const twaManifest = await loadManifest(args, prompt); 60 | const fingerprint: Fingerprint = {name: args.name, value: fingerprintValue}; 61 | twaManifest.fingerprints.push(fingerprint); 62 | prompt.printMessage(enUS.messageAddedFingerprint(fingerprint)); 63 | await saveManifest(args, twaManifest, prompt); 64 | return await generateAssetLinks(args, prompt, twaManifest); 65 | } 66 | 67 | async function removeFingerprint(args: ParsedArgs, prompt: Prompt): Promise { 68 | if (args._.length < 3) { 69 | throw new Error(enUS.errorMissingArgument(3, args._.length)); 70 | } 71 | const fingerprint = args._[2]; 72 | const twaManifest = await loadManifest(args, prompt); 73 | twaManifest.fingerprints = 74 | twaManifest.fingerprints.filter((value) => { 75 | if (value.value === fingerprint) { 76 | prompt.printMessage(enUS.messageRemovedFingerprint(value)); 77 | return false; 78 | } 79 | return true; 80 | }); 81 | await saveManifest(args, twaManifest, prompt); 82 | return await generateAssetLinks(args, prompt, twaManifest); 83 | } 84 | 85 | async function listFingerprints(args: ParsedArgs, prompt: Prompt): Promise { 86 | const twaManifest = await loadManifest(args, prompt); 87 | twaManifest.fingerprints.forEach((fingerprint) => { 88 | console.log(`\t${fingerprint.name || ''}: ${fingerprint.value}`); 89 | }); 90 | return true; 91 | } 92 | 93 | export async function fingerprint( 94 | args: ParsedArgs, 95 | prompt: Prompt = new InquirerPrompt()): Promise { 96 | if (args._.length < 2) { 97 | throw new Error(enUS.errorMissingArgument(2, args._.length)); 98 | } 99 | const subcommand = args._[1]; 100 | switch (subcommand) { 101 | case 'add': 102 | return await addFingerprint(args, prompt); 103 | case 'remove': 104 | return await removeFingerprint(args, prompt); 105 | case 'list': 106 | return await listFingerprints(args, prompt); 107 | case 'generateAssetLinks': 108 | return await generateAssetLinks(args, prompt); 109 | default: 110 | throw new Error(`Unknown subcommand: ${subcommand}`); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/core/src/lib/features/Feature.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class Metadata { 18 | constructor(public readonly name: string, public readonly value: string) {}; 19 | } 20 | 21 | /** 22 | * Specifies a set of customizations to be applied when generating the Android project 23 | * in order to enable a feature. 24 | */ 25 | export interface Feature { 26 | /** 27 | * The feature name. 28 | */ 29 | name: string; 30 | /** 31 | * Customizations to be applied to `app/build.grade`. 32 | */ 33 | buildGradle: { 34 | /** 35 | * Repositories to be added the `repositories` section. 36 | */ 37 | repositories: string[]; 38 | /** 39 | * Dependencies to be added the `dependencies` section. 40 | * The format 'group:name:version' must be used. 41 | * Example `androidx.appcompat:appcompat:1.2.0`. 42 | */ 43 | dependencies: string[]; 44 | /** 45 | * Entries to be added the `android.defaultConfig` section. 46 | */ 47 | configs: string[]; 48 | }; 49 | 50 | /** 51 | * Customizations to be applied to `app/src/main/AndroidManifest.xml`. 52 | */ 53 | androidManifest: { 54 | /** 55 | * Name for permissions required for the app. 56 | * Example: `android.permission.INTERNET`. 57 | */ 58 | permissions: string[]; 59 | /** Components to be added to the app, like activities, services, receivers, etc. 60 | * Example: 61 | * ```xml 62 | * 65 | * 66 | * 67 | * 68 | * ` 69 | * ``` 70 | */ 71 | components: string[]; 72 | /** 73 | * Additional meta-data items to be added into the `application` tag. 74 | */ 75 | applicationMetadata: Metadata[]; 76 | /** 77 | * Additional manifest entries to be added into the `activity` tag of LauncherActivity. 78 | */ 79 | launcherActivityEntries: string[]; 80 | }; 81 | /** 82 | * Customizations to be added to `app/src/main/java//Application.java`. 83 | */ 84 | applicationClass: { 85 | /** 86 | * Imports to be added. Only the class name must be added. Example: 87 | * `android.net.Uri` 88 | */ 89 | imports: string[]; 90 | /** 91 | * Variables to be added to the class. The full declaration is required. Example: 92 | * `private static final String MY_API_ID = "12345";` 93 | */ 94 | variables: string[]; 95 | /** 96 | * Code segment to be added to the `onCreate()` callback for the Application. The code is 97 | * added *after* calling `super.onCreate();`. 98 | */ 99 | onCreate?: string; 100 | }; 101 | /** 102 | * Customizations to be added to `app/src/main/java//LauncherActivity.java`. 103 | */ 104 | launcherActivity: { 105 | /** 106 | * Imports to be added. Only the class name must be added. Example: 107 | * `android.net.Uri` 108 | */ 109 | imports: string[]; 110 | /** 111 | * Variables to be added to the class. The full declaration is required. Example: 112 | * `private static final String MY_API_ID = "12345";` 113 | */ 114 | variables: string[]; 115 | /** 116 | * Methods to be added to the class. The full declaration is required. Example: 117 | * ``` 118 | * private void myMethod() { 119 | * ... // Method implementation. 120 | * } 121 | * ``` 122 | */ 123 | methods: string[]; 124 | /** 125 | * Code segment to be added to the `getLaunchingUrl()`. The code is added *after* calling 126 | * `super.getLaunchingUrl();` and can modify the Uri returned by that. The code will be called 127 | * by each plugin, and the Uri should be extended by calling `Uri.buildUpon`. 128 | * Example: 129 | * ``` 130 | * uri = uri 131 | * .buildUpon() 132 | * .appendQueryParameter("my_extra_parameter", "value") 133 | * .build(); 134 | * ``` 135 | */ 136 | launchUrl?: string; 137 | }; 138 | /** 139 | * Customizations to be added to `app/src/main/java//DelegationService.java`. 140 | */ 141 | delegationService: { 142 | /** 143 | * Imports to be added. Only the class name must be added. Example: 144 | * `android.net.Uri` 145 | */ 146 | imports: string[]; 147 | /** 148 | * Code segment to be added to onCreate. The code will be called by each plugin. 149 | * by each plugin. 150 | */ 151 | onCreate?: string; 152 | }; 153 | } 154 | -------------------------------------------------------------------------------- /packages/cli/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | import {join} from 'path'; 19 | import {homedir} from 'os'; 20 | import {Config, Log, ConsoleLog, JdkHelper, AndroidSdkTools} from '@bubblewrap/core'; 21 | import {JdkInstaller} from './JdkInstaller'; 22 | import {AndroidSdkToolsInstaller} from './AndroidSdkToolsInstaller'; 23 | import {existsSync} from 'fs'; 24 | import {enUS as messages} from './strings'; 25 | import {promises as fsPromises} from 'fs'; 26 | import {InquirerPrompt, Prompt} from './Prompt'; 27 | 28 | const DEFAULT_CONFIG_FOLDER = join(homedir(), '.bubblewrap'); 29 | const DEFAULT_CONFIG_NAME = 'config.json'; 30 | export const DEFAULT_CONFIG_FILE_PATH = join(DEFAULT_CONFIG_FOLDER, DEFAULT_CONFIG_NAME); 31 | const LEGACY_CONFIG_FOLDER = join(homedir(), '.llama-pack'); 32 | const LEGACY_CONFIG_NAME = 'llama-pack-config.json'; 33 | const LEGACY_CONFIG_FILE_PATH = join(LEGACY_CONFIG_FOLDER, LEGACY_CONFIG_NAME); 34 | const DEFAULT_JDK_FOLDER = join(DEFAULT_CONFIG_FOLDER, 'jdk'); 35 | const DEFAULT_SDK_FOLDER = join(DEFAULT_CONFIG_FOLDER, 'android_sdk'); 36 | 37 | async function configAndroidSdk(prompt: Prompt = new InquirerPrompt()): Promise { 38 | const sdkInstallRequest = await prompt.promptConfirm(messages.promptInstallSdk, true); 39 | 40 | let sdkPath; 41 | if (!sdkInstallRequest) { 42 | sdkPath = await prompt.promptInput(messages.promptSdkPath, null, 43 | AndroidSdkTools.validatePath); 44 | } else { 45 | const sdkTermsAgreement = await prompt.promptConfirm(messages.promptSdkTerms, false); 46 | if (sdkTermsAgreement) { 47 | await fsPromises.mkdir(DEFAULT_SDK_FOLDER, {recursive: true}); 48 | prompt.printMessage(messages.messageDownloadSdk + DEFAULT_SDK_FOLDER); 49 | const androidSdkToolsInstaller = new AndroidSdkToolsInstaller(process, prompt); 50 | await androidSdkToolsInstaller.install(DEFAULT_SDK_FOLDER); 51 | sdkPath = DEFAULT_SDK_FOLDER; 52 | } else { 53 | throw new Error(messages.errorSdkTerms); 54 | } 55 | } 56 | 57 | return sdkPath; 58 | } 59 | 60 | async function configureJdk(prompt: Prompt = new InquirerPrompt()): Promise { 61 | const jdkInstallRequest = await prompt.promptConfirm(messages.promptInstallJdk, true); 62 | 63 | let jdkPath; 64 | if (!jdkInstallRequest) { 65 | jdkPath = await prompt.promptInput(messages.promptJdkPath, null, 66 | JdkHelper.validatePath); 67 | } else { 68 | await fsPromises.mkdir(DEFAULT_JDK_FOLDER, {recursive: true}); 69 | prompt.printMessage(messages.messageDownloadJdk + DEFAULT_JDK_FOLDER); 70 | const jdkInstaller = new JdkInstaller(process, prompt); 71 | jdkPath = await jdkInstaller.install(DEFAULT_JDK_FOLDER); 72 | } 73 | return jdkPath; 74 | } 75 | 76 | async function renameConfigIfNeeded(log: Log): Promise { 77 | if (existsSync(DEFAULT_CONFIG_FILE_PATH)) return; 78 | // No new named config file found. 79 | if (!existsSync(LEGACY_CONFIG_FILE_PATH)) return; 80 | // Old named config file found - rename it and its folder. 81 | log.info('An old named config file was found, changing it now'); 82 | const files = await fsPromises.readdir(LEGACY_CONFIG_FOLDER); 83 | const numOfFiles = files.length; 84 | if (numOfFiles != 1) { 85 | // At this point, we know that's at least one file in the folder, `LEGACY_CONFIG_NAME, so 86 | // `numOfFiles' will be at least `1`. We avoid destroying / moving other files in this folder. 87 | await fsPromises.mkdir(DEFAULT_CONFIG_FOLDER); 88 | await fsPromises.rename(LEGACY_CONFIG_FILE_PATH, DEFAULT_CONFIG_FILE_PATH); 89 | } else { 90 | await fsPromises.rename(LEGACY_CONFIG_FOLDER, DEFAULT_CONFIG_FOLDER); 91 | await fsPromises 92 | .rename(join(DEFAULT_CONFIG_FOLDER, LEGACY_CONFIG_NAME), DEFAULT_CONFIG_FILE_PATH); 93 | } 94 | } 95 | 96 | export async function loadOrCreateConfig( 97 | log: Log = new ConsoleLog('config'), 98 | prompt: Prompt = new InquirerPrompt(), 99 | path?: string): Promise { 100 | let configPath; 101 | if (path === undefined) { 102 | await renameConfigIfNeeded(log); 103 | configPath = DEFAULT_CONFIG_FILE_PATH; 104 | } else { 105 | configPath = path; 106 | } 107 | let config = await Config.loadConfig(configPath); 108 | if (!config) { 109 | config = new Config('', ''); 110 | config.saveConfig(configPath); 111 | } 112 | 113 | if (!config.jdkPath) { 114 | const jdkPath = await configureJdk(prompt); 115 | config.jdkPath = jdkPath; 116 | await config.saveConfig(configPath); 117 | } 118 | 119 | if (!config.androidSdkPath) { 120 | const androidSdkPath = await configAndroidSdk(prompt); 121 | config.androidSdkPath = androidSdkPath; 122 | await config.saveConfig(configPath); 123 | } 124 | 125 | return config; 126 | } 127 | -------------------------------------------------------------------------------- /packages/core/src/lib/Log.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * An interface for loggers. 19 | */ 20 | export interface Log { 21 | 22 | /** 23 | * Prints a debug message to the Log. Message is ignored if the Log is not set to verbose. 24 | * @param message the message the be printed. 25 | * @param args extra arguments for the console. 26 | */ 27 | debug(message: string, ...args: string[]): void; 28 | 29 | /** 30 | * Prints an info message to the Log. Message is ignored if the Log is not set to verbose. 31 | * @param message The message the be printed. 32 | * @param args Extra arguments for the console. 33 | */ 34 | info(message: string, ...args: string[]): void; 35 | 36 | /** 37 | * Prints an warning message to the Log. Message is ignored if the Log is not set to verbose. 38 | * @param message The message the be printed. 39 | * @param args Extra arguments for the console. 40 | */ 41 | warn(message: string, ...args: string[]): void; 42 | /** 43 | * Prints an error message to the Log. Message is ignored if the Log is not set to verbose. 44 | * @param message The message the be printed. 45 | * @param args Extra arguments for the console. 46 | */ 47 | error(message: string, ...args: string[]): void; 48 | 49 | setVerbose(verbose: boolean): void; 50 | }; 51 | 52 | /** 53 | * An utility class to print nice Log messages. 54 | */ 55 | export class ConsoleLog implements Log { 56 | private tag: string; 57 | private prefix: string; 58 | private output: Console; 59 | 60 | /** 61 | * The verbosity of the Log. "debug" messages are ignored if verbose is set to false. 62 | */ 63 | public verbose: boolean; 64 | 65 | /** 66 | * Creates a new Log instance 67 | * @param tag The tag used when logging. Printed at the beggining of a log message. 68 | * @param verbose If the Log is verbose. Debug messages are only printed on verbose logs. 69 | * @param output Where to output the log messages. 70 | */ 71 | constructor(tag = '', verbose = false, output = console) { 72 | this.tag = tag; 73 | this.verbose = verbose; 74 | this.prefix = this.inverse(tag); 75 | this.output = output; 76 | } 77 | 78 | /** 79 | * Prints a debug message to the Log. Message is ignored if the Log is not set to verbose. 80 | * @param message The message the be printed. 81 | * @param args Extra arguments for the console. 82 | */ 83 | debug(message: string, ...args: string[]): void { 84 | if (!this.verbose) { 85 | return; 86 | } 87 | this.log(this.output.log, this.dim(message), ...args); 88 | } 89 | 90 | /** 91 | * Prints an info message to the Log. Message is ignored if the Log is not set to verbose. 92 | * @param message The message the be printed. 93 | * @param args Extra arguments for the console. 94 | */ 95 | info(message: string, ...args: string[]): void { 96 | this.log(this.output.log, message, ...args); 97 | } 98 | 99 | /** 100 | * Prints an warning message to the Log. Message is ignored if the Log is not set to verbose. 101 | * @param message The message the be printed. 102 | * @param args Extra arguments for the console. 103 | */ 104 | warn(message: string, ...args: string[]): void { 105 | this.log(this.output.warn, this.yellow('WARNING ' + message), ...args); 106 | } 107 | 108 | /** 109 | * Prints an error message to the Log. Message is ignored if the Log is not set to verbose. 110 | * @param message The message the be printed. 111 | * @param args Extra arguments for the console. 112 | */ 113 | error(message: string, ...args: string[]): void { 114 | this.output.error('\n'); 115 | this.log(this.output.error, this.red('ERROR ' + message), ...args); 116 | this.output.error('\n'); 117 | } 118 | 119 | /** 120 | * Sets the verbose. 121 | * @param verbose The verbose value to set. 122 | */ 123 | setVerbose(verbose: boolean): void { 124 | this.verbose = verbose; 125 | } 126 | 127 | /** 128 | * Creates a new Log using the same output and verbositity of the current Log. 129 | * @param newTag The tag the be used on the new Log instance. 130 | */ 131 | newLog(newTag: string): ConsoleLog { 132 | if (this.tag) { 133 | newTag = this.tag + ' ' + newTag; 134 | } 135 | return new ConsoleLog(newTag, this.verbose, this.output); 136 | } 137 | 138 | private log(fn: Function, message: string, ...args: string[]): void { 139 | if (this.prefix) { 140 | message = this.prefix + ' ' + message; 141 | } 142 | if (args) { 143 | fn(...[message].concat(args)); 144 | } else { 145 | fn(message); 146 | } 147 | } 148 | 149 | private inverse(input: string): string { 150 | return `\x1b[7m${input}\x1b[0m`; 151 | } 152 | 153 | private dim(input: string): string { 154 | return `\x1b[36m${input}\x1b[0m`; 155 | } 156 | 157 | private yellow(input: string): string { 158 | return `\x1b[33m${input}\x1b[0m`; 159 | } 160 | 161 | private red(input: string): string { 162 | return `\x1b[31m${input}\x1b[0m`; 163 | } 164 | } 165 | --------------------------------------------------------------------------------