├── .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 | 
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 | [](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 |
--------------------------------------------------------------------------------