Repository: eclipse-theia/theia-ide Branch: master Commit: c5ce2e7120a1 Files: 136 Total size: 351.1 KB Directory structure: gitextract_jxq8o1lq/ ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── build-next-release.yml │ ├── build-next.yml │ ├── build.yml │ ├── license-check-workflow.yml │ ├── publish-builder-img.yml │ └── publish-theia-ide-img.yml ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── theia.code-snippets ├── ADOPTER.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── NOTICE.md ├── PUBLISHING.md ├── README.md ├── TheiaIDE logo/ │ └── TheiaIDE.eps ├── applications/ │ ├── browser/ │ │ ├── package.json │ │ ├── resources/ │ │ │ └── preload.html │ │ ├── tsconfig.json │ │ └── webpack.config.js │ ├── electron/ │ │ ├── .eslintrc.js │ │ ├── electron-builder.yml │ │ ├── entitlements.plist │ │ ├── package.json │ │ ├── resources/ │ │ │ ├── LICENSE │ │ │ ├── icon.icns │ │ │ ├── icons/ │ │ │ │ └── MacLauncherIcons/ │ │ │ │ ├── 512-512-2.icns │ │ │ │ ├── Theia-16bp-alfa ignored.icns │ │ │ │ ├── icns-1bit/ │ │ │ │ │ ├── 128-128.icns │ │ │ │ │ ├── 16-16-1.icns │ │ │ │ │ ├── 256-256.icns │ │ │ │ │ ├── 32-32.icns │ │ │ │ │ ├── 48-48.icns │ │ │ │ │ └── 512-512-2 copy.icns │ │ │ │ ├── icns-8bit/ │ │ │ │ │ ├── 128-128.icns │ │ │ │ │ ├── 16-16.icns │ │ │ │ │ ├── 256-256.icns │ │ │ │ │ ├── 32-32.icns │ │ │ │ │ ├── 48-48.icns │ │ │ │ │ └── 512-512.icns │ │ │ │ ├── icon.icns │ │ │ │ └── icon.icon/ │ │ │ │ └── icon.json │ │ │ └── preload.html │ │ ├── scripts/ │ │ │ ├── after-pack.js │ │ │ ├── appimage-helpers.js │ │ │ ├── generate-app-update-yml.js │ │ │ ├── notarize.sh │ │ │ ├── sign-directory-windows.ts │ │ │ ├── sign-directory.ts │ │ │ ├── sign-utils.ts │ │ │ ├── sign.sh │ │ │ ├── theia-electron-main.js │ │ │ ├── update-blockmap.ts │ │ │ └── update-checksum.ts │ │ ├── test/ │ │ │ ├── app.spec.js │ │ │ └── workspace/ │ │ │ └── README.md │ │ ├── tsconfig.eslint.json │ │ ├── tsconfig.json │ │ └── webpack.config.js │ └── electron-next/ │ ├── .eslintrc.js │ ├── electron-builder.yml │ ├── package.json │ ├── resources/ │ │ ├── LICENSE │ │ └── preload.html │ ├── scripts/ │ │ ├── after-pack.js │ │ ├── appimage-helpers.js │ │ └── theia-electron-main.js │ ├── test/ │ │ └── workspace/ │ │ └── README.md │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ └── webpack.config.js ├── browser.Dockerfile ├── cleanup/ │ └── Jenkinsfile ├── configs/ │ ├── base.eslintrc.json │ ├── base.tsconfig.json │ ├── build.eslintrc.json │ ├── errors.eslintrc.json │ ├── license-check-config.json │ ├── tsconfig.eslint.json │ ├── warnings.eslintrc.json │ └── xss.eslintrc.json ├── docs/ │ └── developing-with-local-theia.md ├── lerna.json ├── next/ │ ├── Jenkinsfile │ └── NEXT_INTEGRATION_BUILD.md ├── package.json ├── patches/ │ └── @theia+terminal+1.72.0-next.20.patch ├── releng/ │ ├── preview/ │ │ ├── Jenkinsfile.build │ │ ├── Jenkinsfile.sign │ │ └── Jenkinsfile.upload │ └── promote/ │ └── Jenkinsfile ├── scripts/ │ ├── build-with-local-theia.js │ ├── generate-next-icons.js │ ├── make-files-writeable.ts │ └── update-theia-version.ts ├── theia-extensions/ │ ├── launcher/ │ │ ├── .eslintrc.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── browser/ │ │ │ │ ├── create-launcher-contribution.ts │ │ │ │ ├── create-launcher-frontend-module.ts │ │ │ │ ├── desktopfile-service.ts │ │ │ │ └── launcher-service.ts │ │ │ └── node/ │ │ │ ├── desktopfile-endpoint.ts │ │ │ ├── launcher-backend-module.ts │ │ │ ├── launcher-endpoint.ts │ │ │ └── launcher-util.ts │ │ └── tsconfig.json │ ├── product/ │ │ ├── .eslintrc.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── browser/ │ │ │ │ ├── branding-util.tsx │ │ │ │ ├── style/ │ │ │ │ │ └── index.css │ │ │ │ ├── theia-ide-about-dialog.tsx │ │ │ │ ├── theia-ide-config.ts │ │ │ │ ├── theia-ide-contribution.tsx │ │ │ │ ├── theia-ide-frontend-module.ts │ │ │ │ └── theia-ide-getting-started-widget.tsx │ │ │ └── electron-main/ │ │ │ ├── icon-contribution.ts │ │ │ └── theia-ide-main-module.ts │ │ └── tsconfig.json │ └── updater/ │ ├── .eslintrc.js │ ├── package.json │ ├── src/ │ │ ├── common/ │ │ │ └── updater/ │ │ │ └── theia-updater.ts │ │ ├── electron-browser/ │ │ │ ├── theia-updater-frontend-module.ts │ │ │ └── updater/ │ │ │ ├── theia-updater-frontend-contribution.ts │ │ │ └── theia-updater-preferences.ts │ │ └── electron-main/ │ │ └── update/ │ │ ├── theia-updater-impl.ts │ │ └── theia-updater-main-module.ts │ └── tsconfig.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ /** @type {import('eslint').Linter.Config} */ module.exports = { root: true, extends: [ './configs/base.eslintrc.json', './configs/warnings.eslintrc.json', './configs/errors.eslintrc.json', './configs/xss.eslintrc.json' ], ignorePatterns: [ '**/{node_modules,lib}', 'plugins' ], parserOptions: { tsconfigRootDir: __dirname, project: ['./configs/tsconfig.eslint.json', './theia-extensions/*/tsconfig.json', 'applications/electron/tsconfig.eslint.json'] } }; ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug Report about: Create a report to help us improve --- ### Bug Description: ### Steps to Reproduce: 1. 2. 3. ### Additional Information - Operating System: - Theia Version: ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Question url: https://github.com/eclipse-theia/theia/discussions about: Please ask questions here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: Propose an idea for the project --- ### Feature Description: ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ #### What it does #### How to test #### Review checklist - [ ] as an author, I have thoroughly tested my changes and carefully followed [the review guidelines](https://github.com/theia-ide/theia/blob/master/doc/pull-requests.md#requesting-a-review) #### Reminder for reviewers - as a reviewer, I agree to behave in accordance with [the review guidelines](https://github.com/theia-ide/theia/blob/master/doc/pull-requests.md#reviewing) ================================================ FILE: .github/workflows/build-next-release.yml ================================================ name: Build Theia IDE Next Release (Linux) on: workflow_dispatch: schedule: - cron: "0 2 * * 1-5" # Runs every weekday at 2am UTC jobs: build: name: Build Next Release (Linux only) runs-on: ubuntu-22.04 timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - name: Use Node.js 24 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 24.x registry-url: "https://registry.npmjs.org" - name: Use Python 3.13 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - name: Export build variables shell: bash run: | THEIA_VERSION=next echo "THEIA_VERSION=${THEIA_VERSION}" >> $GITHUB_ENV yarn version --minor --no-git-tag-version IDE_VERSION=$(jq -r .version package.json) echo "THEIA_IDE_VERSION=${IDE_VERSION}-next-$(date +%Y-%m-%d)" >> $GITHUB_ENV - name: Build next package shell: bash run: | yarn --skip-integrity-check --network-timeout 100000 yarn version --new-version $THEIA_IDE_VERSION --no-git-tag-version yarn lerna version $THEIA_IDE_VERSION --no-push --no-git-tag-version --yes yarn update:theia $THEIA_VERSION && yarn update:theia:children $THEIA_VERSION yarn --skip-integrity-check --network-timeout 100000 yarn build:extensions yarn build:applications:next yarn download:plugins yarn package:applications:next env: NODE_OPTIONS: --max_old_space_size=4096 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Test (Linux) run: xvfb-run -a yarn --cwd applications/electron-next test - name: Upload Linux Dist Files uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: theia-ide-next-linux path: | applications/electron-next/dist/TheiaIDENext.AppImage retention-days: 7 - name: Publish to rolling next release shell: bash run: | # Delete existing next release if present gh release delete next --yes --cleanup-tag 2>/dev/null || true # Create a new pre-release with the next tag gh release create next \ --title "Next Build - Ubuntu AppImage (${{ env.THEIA_IDE_VERSION }})" \ --notes "Automated next build of the Ubuntu AppImage from \`master\` ($(date -u '+%Y-%m-%d %H:%M UTC'))." \ --prerelease \ --target ${{ github.sha }} \ applications/electron-next/dist/TheiaIDENext.AppImage \ applications/electron-next/dist/latest-linux.yml env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/build-next.yml ================================================ name: Build Theia IDE next version on: workflow_dispatch: schedule: - cron: "0 3 * * 1" # Runs every monday at 3am jobs: build: name: Build ${{ matrix.os }} next strategy: fail-fast: false matrix: os: [ubuntu-22.04, macos-15, macos-15-intel, windows-2022] # macos-15-intel is for x64, macOS-15 is for arm64 runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # To fetch all history for all branches and tags. (Will be required for caching with lerna: https://github.com/markuplint/markuplint/pull/111) - name: Use Node.js 24 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: 24.x registry-url: "https://registry.npmjs.org" - name: Use Python 3.11 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - name: Export build variables shell: bash run: | THEIA_VERSION=next echo "THEIA_VERSION=${THEIA_VERSION}" >> $GITHUB_ENV yarn version --minor --no-git-tag-version echo "THEIA_IDE_VERSION=$(jq -r .version package.json)-${THEIA_VERSION}-$(date +%d-%m-%y)" >> $GITHUB_ENV - name: Update electron-builder.yml for macos-15 if: matrix.os == 'macos-15' run: | sed -i '' 's|https://download.eclipse.org/theia/ide/latest/macos|https://download.eclipse.org/theia/ide/latest/macos-arm|g' applications/electron/electron-builder.yml - name: Build prod package shell: bash run: | yarn --skip-integrity-check --network-timeout 100000 yarn version --new-version $THEIA_IDE_VERSION --no-git-tag-version yarn lerna version $THEIA_IDE_VERSION --no-push --no-git-tag-version --yes yarn update:theia $THEIA_VERSION && yarn update:theia:children $THEIA_VERSION yarn --skip-integrity-check --network-timeout 100000 yarn build yarn download:plugins yarn package:applications env: NODE_OPTIONS: --max_old_space_size=4096 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9 - name: Rename AppImage with version if: runner.os == 'Linux' run: | mv applications/electron/dist/TheiaIDE.AppImage applications/electron/dist/TheiaIDE-$THEIA_IDE_VERSION.AppImage - name: Upload Linux Dist Files if: runner.os == 'Linux' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: theia-ide-next-linux path: | applications/electron/dist/TheiaIDE-${{ env.THEIA_IDE_VERSION }}.AppImage retention-days: 7 - name: Upload Mac Dist Files if: runner.os == 'macOS' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ matrix.os == 'macos-15-intel' && 'theia-ide-next-mac-x64' || matrix.os == 'macos-15' && 'theia-ide-next-mac-arm64'}} path: applications/electron/dist/*.dmg retention-days: 7 - name: Upload Windows Dist Files if: runner.os == 'Windows' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: theia-ide-next-windows path: applications/electron/dist/*.exe retention-days: 7 ================================================ FILE: .github/workflows/build.yml ================================================ name: Build, package and test on: push: branches: - master paths-ignore: - '**/*.md' - 'TheiaIDE logo/**' workflow_dispatch: pull_request: branches: - master paths-ignore: - '**/*.md' - 'TheiaIDE logo/**' schedule: - cron: "0 4 * * *" # Runs every day at 4am: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule jobs: build: name: ${{ matrix.os }}, Node.js v${{ matrix.node }} strategy: fail-fast: false matrix: os: [windows-2022, ubuntu-22.04, macos-15, macos-15-intel] # macos-15-intel is for x64, macOS-15 is for arm64 node: ["24.x"] runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # To fetch all history for all branches and tags. (Will be required for caching with lerna: https://github.com/markuplint/markuplint/pull/111) - name: Use Node.js ${{ matrix.node }} uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ matrix.node }} registry-url: "https://registry.npmjs.org" - name: Use Python 3.13 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - name: Install dependencies shell: bash run: yarn --skip-integrity-check --network-timeout 100000 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Lint shell: bash run: yarn lint - name: Build dev package (Windows, Linux) if: (runner.os == 'Windows' || runner.os == 'Linux') && github.event_name == 'pull_request' shell: bash run: | yarn build:dev yarn download:plugins yarn package:applications:preview env: NODE_OPTIONS: --max_old_space_size=4096 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9 - name: Build prod package (Windows, Linux) if: (runner.os == 'Windows' || runner.os == 'Linux') && github.event_name != 'pull_request' shell: bash run: | yarn build yarn download:plugins yarn package:applications env: NODE_OPTIONS: --max_old_space_size=4096 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9 - name: Update electron-builder.yml for macos-15 if: matrix.os == 'macos-15' run: | sed -i '' 's|https://download.eclipse.org/theia/ide/latest/macos|https://download.eclipse.org/theia/ide/latest/macos-arm|g' applications/electron/electron-builder.yml - name: Build prod package (Mac) if: runner.os == 'macOS' shell: bash run: | yarn build yarn download:plugins yarn package:applications env: NODE_OPTIONS: --max_old_space_size=4096 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9 - name: Upload Mac Dist Files if: runner.os == 'macOS' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ matrix.os == 'macos-15-intel' && 'mac-x64' || matrix.os == 'macos-15' && 'mac-arm64'}} path: | applications/electron/dist/** !applications/electron/dist/mac/** !applications/electron/dist/mac-arm64/** retention-days: 1 - name: Upload Windows Dist Files if: runner.os == 'Windows' && github.event_name != 'pull_request' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: windows path: | applications/electron/dist/** retention-days: 1 - name: Upload Linux Dist Files if: runner.os == 'Linux' && github.event_name != 'pull_request' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: linux path: | applications/electron/dist/** retention-days: 1 - name: Test (Linux) if: runner.os == 'Linux' run: | xvfb-run -a yarn electron test - name: Test (Windows) if: runner.os == 'Windows' shell: bash run: | yarn electron test - name: Test (macOS) if: runner.os == 'macOS' shell: bash run: | yarn electron test - name: Upload test screenshots if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: test-screenshots-${{ matrix.os }} path: applications/electron/test-screenshots/ if-no-files-found: ignore retention-days: 5 ================================================ FILE: .github/workflows/license-check-workflow.yml ================================================ name: 3PP License Check on: push: branches: - master paths: - 'yarn.lock' workflow_dispatch: pull_request: branches: - master paths: - 'yarn.lock' schedule: - cron: '0 4 * * *' # Runs every day at 4am: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule jobs: License-check: name: 3PP License Check using dash-licenses strategy: fail-fast: false matrix: os: [ubuntu-latest] node: [24] java: [17] runs-on: ${{ matrix.os }} timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 2 - name: Use Node.js ${{ matrix.node }} uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ matrix.node }} - name: Use Java ${{ matrix.java }} uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'adopt' java-version: ${{ matrix.java }} - name: Run daily dash-licenses check (non-review mode) if: ${{ github.event_name == 'schedule' }} shell: bash run: | yarn --frozen-lockfile --ignore-scripts # Workaround: pre-download dash-licenses JAR (see comments in step below) DASH_JAR="node_modules/@eclipse-dash/nodejs-wrapper/download/dash-licenses.jar" mkdir -p "$(dirname "$DASH_JAR")" SNAP_BASE="https://repo.eclipse.org/repository/dash-maven2-snapshots/org/eclipse/dash/org.eclipse.dash.licenses/1.1.1-SNAPSHOT" SNAP_JAR=$(curl -s "$SNAP_BASE/maven-metadata.xml" | grep -oP '(?<=)1\.1\.1-[^<]+' | tail -1) curl -L "$SNAP_BASE/org.eclipse.dash.licenses-$SNAP_JAR.jar" -o "$DASH_JAR" yarn license:check - name: Run dash-licenses check in review mode if: ${{ github.event_name != 'schedule' }} shell: bash run: | yarn --frozen-lockfile --ignore-scripts # Workaround: pre-download dash-licenses JAR since the @eclipse-dash/nodejs-wrapper # uses a broken Nexus 2 URL after the repo.eclipse.org upgrade to Nexus 3. # We use 1.1.1-SNAPSHOT because 1.0.2 and 1.1.0 incorrectly flag internal # workspace packages (link:true with no version) as restricted. # See: https://github.com/eclipse-dash/nodejs-wrapper/issues/7 # See: https://github.com/eclipse-dash/dash-licenses/issues/534 # TODO: remove this workaround once the nodejs-wrapper is updated (GH-17168) DASH_JAR="node_modules/@eclipse-dash/nodejs-wrapper/download/dash-licenses.jar" mkdir -p "$(dirname "$DASH_JAR")" SNAP_BASE="https://repo.eclipse.org/repository/dash-maven2-snapshots/org/eclipse/dash/org.eclipse.dash.licenses/1.1.1-SNAPSHOT" SNAP_JAR=$(curl -s "$SNAP_BASE/maven-metadata.xml" | grep -oP '(?<=)1\.1\.1-[^<]+' | tail -1) curl -L "$SNAP_BASE/org.eclipse.dash.licenses-$SNAP_JAR.jar" -o "$DASH_JAR" yarn license:check:review env: DASH_TOKEN: ${{ secrets.DASH_LICENSES_PAT }} ================================================ FILE: .github/workflows/publish-builder-img.yml ================================================ name: Publish builder image on: schedule: - cron: "0 0 1 * *" # runs 1st day of every month workflow_dispatch: jobs: build: name: Build and push builder image to Docker Hub runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Login to Docker Hub uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Docker image uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . push: true tags: eclipsetheia/theia-blueprint:builder ================================================ FILE: .github/workflows/publish-theia-ide-img.yml ================================================ name: Publish Theia IDE Docker Image on: workflow_dispatch: inputs: tag: description: The image's tag required: true default: next jobs: build: name: Build and push Theia IDE image to Github Packages runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: install: true driver: docker-container driver-opts: | image=moby/buildkit:latest network=host - name: Login to Docker Hub uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 4.1.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to the Github Container registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # 4.1.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: List docker buildx available platforms shell: bash run: | docker buildx inspect --bootstrap - name: Build and push Docker image uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . file: browser.Dockerfile push: true tags: | ghcr.io/${{ github.repository }}/theia-ide:${{ github.event.inputs.tag }} ghcr.io/${{ github.repository }}/theia-ide:latest platforms: linux/amd64,linux/arm64 ================================================ FILE: .gitignore ================================================ .DS_Store **/node_modules **/.browser_modules **/dist **/lib **/src-gen **/gen-webpack.config.js **/gen-webpack.node.config.js **/plugins **/tsconfig.tsbuildinfo *.log license-check-summary.txt* .theia-ide/chatSessions .prompts **/test-screenshots ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible Node.js debug attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Attach by Process ID", "processId": "${command:PickProcess}" }, { "type": "node", "request": "launch", "name": "Launch with Node.js", "program": "${file}" }, { "type": "node", "request": "launch", "name": "Launch Electron Backend", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" }, "cwd": "${workspaceFolder}/applications/electron", "protocol": "inspector", "args": [ "scripts/theia-electron-main.js", "--log-level=debug", "--hostname=localhost", "--no-cluster", "--app-project-path=${workspaceFolder}/applications/electron", "--remote-debugging-port=9222", "--no-app-auto-install", "--plugins=local-dir:../../plugins" ], "env": { "NODE_ENV": "development" }, "sourceMaps": true, "outFiles": [ "${workspaceFolder}/applications/electron/src-gen/**/*.js", "${workspaceFolder}/applications/electron/lib/**/*.js", "${workspaceFolder}/theia-extensions/*/lib/**/*.js", "${workspaceFolder}/node_modules/@theia/*/lib/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" }, { "type": "node", "request": "launch", "name": "Launch Browser Backend", "program": "${workspaceFolder}/applications/browser/lib/backend/main.js", "cwd": "${workspaceFolder}/applications/browser", "args": [ "--hostname=0.0.0.0", "--port=3000", "--no-cluster", "--app-project-path=${workspaceFolder}/applications/browser", "--plugins=local-dir:../../plugins", "--log-level=debug" ], "env": { "NODE_ENV": "development" }, "sourceMaps": true, "outFiles": [ "${workspaceFolder}/applications/browser/src-gen/**/*.js", "${workspaceFolder}/applications/browser/lib/**/*.js", "${workspaceFolder}/theia-extensions/**/lib/**/*.js", "${workspaceFolder}/node_modules/@theia/*/lib/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" }, { "type": "node", "request": "attach", "name": "Attach to Plugin Host", "port": 9339, "timeout": 60000, "stopOnEntry": false, "smartStep": true, "sourceMaps": true, "internalConsoleOptions": "openOnSessionStart", "outFiles": [ "${workspaceFolder}/plugins/**/*.js" ] }, { "type": "node", "request": "launch", "name": "Launch Browser Backend (eclipse.jdt.ls)", "program": "${workspaceFolder}/applications/browser/lib/backend/main.js", "cwd": "${workspaceFolder}/applications/browser", "args": [ "--hostname=0.0.0.0", "--port=3000", "--no-cluster", "--root-dir=${workspaceFolder}/../eclipse.jdt.ls/org.eclipse.jdt.ls.core", "--app-project-path=${workspaceFolder}/applications/browser", "--plugins=local-dir:../../plugins", "--log-level=debug", "--no-app-auto-install" ], "env": { "NODE_ENV": "development" }, "sourceMaps": true, "outFiles": [ "${workspaceFolder}/applications/browser/src-gen/**/*.js", "${workspaceFolder}/applications/browser/lib/**/*.js", "${workspaceFolder}/theia-extensions/**/lib/**/*.js", "${workspaceFolder}/node_modules/@theia/*/lib/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" }, { "type": "node", "request": "launch", "protocol": "inspector", "name": "Run Mocha Tests", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", "args": [ "--no-timeouts", "--colors", "--opts", "${workspaceFolder}/configs/mocha.opts", "**/${fileBasenameNoExtension}.js" ], "env": { "TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json" }, "sourceMaps": true, "smartStep": true, "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" }, { "name": "Launch Browser Frontend", "type": "chrome", "request": "launch", "url": "http://localhost:3000/", "webRoot": "${workspaceFolder}/applications/browser" }, { "type": "chrome", "request": "attach", "name": "Attach to Electron Frontend", "port": 9222, "webRoot": "${workspaceFolder}/applications/electron" }, { "name": "Launch VS Code Tests", "type": "node", "request": "launch", "args": [ "${workspaceFolder}/applications/browser/lib/backend/main.js", "${workspaceFolder}/plugins/vscode-api-tests/testWorkspace", "--port", "3030", "--hostname", "0.0.0.0", "--extensionTestsPath=${workspaceFolder}/plugins/vscode-api-tests/out/singlefolder-tests", "--hosted-plugin-inspect=9339" ], "env": { "THEIA_DEFAULT_PLUGINS": "local-dir:${workspaceFolder}/plugins" }, "stopOnEntry": false, "sourceMaps": true, "outFiles": [ "${workspaceFolder}/../.js" ] } ], "compounds": [ { "name": "Launch Electron Backend & Frontend", "configurations": [ "Launch Electron Backend", "Attach to Plugin Host", "Attach to Electron Frontend" ], "stopAll": true }, { "name": "Launch Browser Backend & Frontend", "configurations": [ "Launch Browser Backend", "Attach to Plugin Host", "Launch Browser Frontend" ], "stopAll": true } ] } ================================================ FILE: .vscode/settings.json ================================================ { "[markdown]": { "editor.defaultFormatter": "davidanson.vscode-markdownlint" } } ================================================ FILE: .vscode/theia.code-snippets ================================================ { "Copyright-JS/JSX/TS/TSX/CSS": { "prefix": [ "header", "copyright" ], "body": "/********************************************************************************\n * Copyright (C) $CURRENT_YEAR ${YourCompany} and others.\n *\n * This program and the accompanying materials are made available under the\n * terms of the MIT License, which is available in the project root.\n *\n * SPDX-License-Identifier: MIT\n ********************************************************************************/\n\n$0", "description": "Adds the copyright...", "scope": "javascript,javascriptreact,typescript,typescriptreact,css" } } ================================================ FILE: ADOPTER.md ================================================ # Adopter Guide This repository serves as a template for building desktop products on the [Eclipse Theia platform](https://theia-ide.org). This guide documents packaging considerations when building your own Theia based applications. ## Electron Packaging and Asar Electron applications may use [asar archives](https://github.com/electron/asar) to package application source files into a single read only archive. This improves startup performance and avoids path length issues on Windows. In this repository, asar packaging is enabled via `asar: true` in `applications/electron/electron-builder.yml`. When the application is packaged with `electron-builder`, the contents of the app directory are bundled into an `app.asar` file inside the packaged application's `resources/` folder. Code that uses `__dirname` to access files, or executes scripts from the filesystem, will fail because asar archives are not real directories. At runtime, `__dirname` resolves to a path inside `app.asar`, but Node's `fs` module and the OS cannot access files inside the archive as regular filesystem entries. Two categories of files are typically affected: 1. **Native binaries**, `.node` files or prebuilt binaries 2. **Scripts and resources** that must be accessible on the real filesystem ### Mitigation Strategies #### 1. asarUnpack (electron-builder.yml) Files matching `asarUnpack` glob patterns are automatically extracted alongside the asar archive during packaging. At runtime, these files live under `app.asar.unpacked/` instead of `app.asar/`. For example: ```yaml asarUnpack: - "**/lib/backend/native/**" - "**/lib/backend/shell-integrations/**" - "**/lib/build/Release/**" - "**/lib/prebuilds/**" ``` `asarUnpack` only extracts the files to the real filesystem. Code that resolves paths using `__dirname` will still get a path containing `app.asar`, so it must also handle the `.asar.unpacked` path segment for the files to be found. #### 2. patch-package When upstream code in `node_modules` needs modification, for example to adjust paths, [patch-package](https://github.com/ds300/patch-package) can apply source level patches after `yarn install`. * Patches live in the `patches` directory at the repository root * Patches are applied automatically via the `postinstall` script in `package.json` #### 3. Webpack Post Processing Another option is to adjust the bundled code in Webpack. As an example, the `PatchRipgrepPlugin` in `applications/electron/webpack.config.js` patches the bundled `main.js` after webpack emit to rewrite ripgrep's path resolution from `.asar` to `.asar.unpacked`: ```js class PatchRipgrepPlugin { apply(compiler) { compiler.hooks.afterEmit.tapAsync('PatchRipgrepPlugin', (compilation, callback) => { // Reads main.js, finds the ripgrep path assignment, and rewrites // .asar + path.sep → .asar.unpacked + path.sep // ... }); } } ``` ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Community Code of Conduct Version 1.1 October 21, 2019 ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at codeofconduct@eclipse.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ---- Note: Please see [here](https://www.eclipse.org/org/documents/Community_Code_of_Conduct.php) for the latest version of this document, hosted at the Eclipse Foundation ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Eclipse Theia Theia is a young open-source project with a modular architecture. One of the goals is to make sure that we can customize and enhance any Theia application through extensions. So while the main Theia repository contains some common functionality for IDE-like applications, like a file system or a navigator view, most functionality doesn't necessarily need to be put into the core repository but can be developed separately. ## How Can I Contribute? In the following some of the typical ways of contribution are described. ### Asking Questions It's totally fine to ask questions by opening an issue in the Theia GitHub repository. We will close it once it's answered and tag it with the 'question' label. Please check if the question has been asked before there or on [Stack Overflow](https://stackoverflow.com). ### Reporting Bugs If you have found a bug, you should first check if it has already been filed and maybe even fixed. If you find an existing unresolved issue, please add your case. If you could not find an existing bug report, please file a new one. In any case, please add all information you can share and that will help to reproduce and solve the problem. ### Reporting Feature Requests You may want to see a feature or have an idea. You can file a request and we can discuss it. If such a feature request already exists, please add a comment or some other form of feedback to indicate you are interested too. Also in this case any concrete use case scenario is appreciated to understand the motivation behind it. ### Pull Requests Before you get started investing significant time in something you want to get merged and maintained as part of Theia, you should talk with the team through an issue. Simply choose the issue you would want to work on, and tell everyone that you are willing to do so and how you would approach it. The team will be happy to guide you and give feedback. We follow the contributing and reviewing pull request guidelines described [here](https://github.com/eclipse-theia/theia/blob/master/doc/pull-requests.md). ## Coding Guidelines We follow the coding guidelines described [here](https://github.com/eclipse-theia/theia/wiki/Coding-Guidelines). ## Eclipse Contributor Agreement Before your contribution can be accepted by the project team contributors must electronically sign the Eclipse Contributor Agreement (ECA). * https://www.eclipse.org/legal/ECA.php Commits that are provided by non-committers must have a Signed-off-by field in the footer indicating that the author is aware of the terms by which the contribution has been provided to the project. The non-committer must additionally have an Eclipse Foundation account and must have a signed Eclipse Contributor Agreement (ECA) on file. For more information, please see the Eclipse Committer Handbook: https://www.eclipse.org/projects/handbook/#resources-commit ## Sign your work The sign-off is a simple line at the end of the explanation for the patch. Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from [developercertificate.org](https://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 1 Letterman Drive Suite D4700 San Francisco, CA, 94129 Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` Then you just add a line to every git commit message: Signed-off-by: Joe Smith Use your real name (sorry, no pseudonyms or anonymous contributions.) If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`. ================================================ FILE: Dockerfile ================================================ # See the associated GitHub workflow, that builds and publishes # this docker image to Docker Hub: # .github/workflows/publish-builder-img.yml # It can be triggered manually from the GitHub project page. # We want to support as many Debian versions as possible. # Therefore, use the oldest Debian release that still provides the desired Node.js version. FROM node:24-bookworm RUN dpkg --add-architecture i386 \ && apt-get update \ && apt-get install -y --no-install-recommends \ libxkbfile-dev libsecret-1-dev python3 \ wine wine32 wine64 \ && rm -rf /var/lib/apt/lists/* # UID-agnostic wine tuning. WINEPREFIX is intentionally NOT set here; callers # must provide a writable path owned by the runtime user. ENV WINEDLLOVERRIDES="mscoree=;mshtml=" \ WINEDEBUG=-all ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Eclipse Theia IDE Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NOTICE.md ================================================ # Notices for Eclipse Theia This content is produced and maintained by the Eclipse Theia project. * Project home: https://projects.eclipse.org/projects/ecd.theia ## Trademarks Eclipse Theia is a trademark of the Eclipse Foundation. ## Copyright All content is the property of the respective authors or their employers. For more information regarding authorship of content, please consult the listed source code repository logs. ## Declared Project Licenses This program and the accompanying materials are made available under the terms of the MIT License which is available at https://opensource.org/license/mit/. SPDX-License-Identifier: MIT ## Source Code The project maintains the following source code repositories: * https://github.com/eclipse/theia-generator-plugin * https://github.com/eclipse/theia-yeoman-plugin * https://github.com/eclipse/theia-plugin-packager * https://github.com/eclipse/theia-cpp-extension * https://github.com/eclipse/theia-python-extension * https://github.com/eclipse/theia-java-extension ## Third-party Content This project leverages the following third party content. chalk (2.4.1) * License: MIT * Project: https://github.com/chalk/chalk * Source: https://github.com/chalk/chalk code copied from project cortex-debug (0.1.21) * License: MIT Code copied from project Microsoft/vscode (1.31.0) * License: MIT Code copied from project Microsoft/vscode (1.32.3) * License: MIT * Project: https://code.visualstudio.com/ * Source: https://github.com/Microsoft/vscode Code copied from project Microsoft/vscode (1.33.1) * License: MIT code copied from project microsoft/vscode (1.33.1) * License: MIT Code copied from VSCode (n/a) * License: MIT Copied code from project VSCode (n/a) * License: MIT CSS copied from VS Code (n/a) * License: MIT dugite (1.52.0) * License: MIT Electron (3.1.7) * License: BSD-2-Clause AND BSD-3-Clause AND (MIT OR GPL-2.0) AND Apache-2.0 AND (BSD-2-Clause OR MIT OR Apache-2.0) AND ISC AND MIT AND X11 AND BSD-2-Clause-FreeBSD AND Public-Domain AND Unlicense AND MPL-2.0 AND (BSD-3-Clause OR MPL-2.0) AND CC-BY-3.0 AND (AFL-2.0 * Project: https://electronjs.org/ * Source: https://github.com/electron/electron electron@2.0.14 (2.0.14) * License: MIT AND BSD-2-Clause AND Apache-2.0 AND (AFL-2.1 OR BSD-3-Clause) AND BSD-3-Clause AND ISC AND X11 AND Public-Domain AND (GPL-2.0 OR MIT) AND Unlicense AND IJG AND ICU AND UNICODE-TOU AND NTP AND (MIT OR BSD-3-Clause) AND Libpng AND MPL-2.0 AND LGPL-2.1+ * Project: https://github.com/electron/electron * Source: https://github.com/electron/electron/releases/tag/v2.0.14 getmac (1.4.6) * License: MIT * Project: https://github.com/bevry/getmac * Source: https://github.com/bevry/getmac GH-3397: Implemented the HTTP-based authentication for Git in Electron. (n/a) * License: MIT glob promise (3.4.0) * License: ISC * Project: https://github.com/ahmadnassri/glob-promise * Source: https://github.com/ahmadnassri/glob-promise Icon configure-inverse.svg (n/a) * License: MIT * Project: https://github.com/Microsoft/vscode * Source: https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/contrib/tasks/common/media/configure-inverse.svg#L1 libffmpeg (FFmpeg) Delivered with Electron (3.1.7) * License: LGPL-2.1+ long.js (3.2.0) * License: Apache-2.0 micromatch (3.1.10) * License: MIT * Project: https://github.com/micromatch/micromatch * Source: https://github.com/micromatch/micromatch monaco-typescript (2.3.0) * License: MIT * Project: https://github.com/Microsoft/monaco-typescript * Source: https://github.com/Microsoft/monaco-typescript.git native-keymap (1.2.5) * License: Pending * Project: https://github.com/Microsoft/node-native-keymap * Source: https://github.com/Microsoft/node-native-keymap node-oniguruma (n/a) * License: BSD-2-Clause AND GPL-2.0 WITH Autoconf-exception-2.0 AND GPL-2.0-or-later WITH libtool-exception AND X11 AND MIT AND Public-Domain node.js dependencies for Theia (n/a) * License: MIT AND BSD-3-Clause AND ISC AND Apache-2.0 AND BSD-2-Clause AND Zlib AND X11 AND (BSD-3-Clause OR AFL-2.1) AND CC-By-4.0 AND CC-by-2.5-SA AND CC0-1.0 AND (BSD-3-Clause OR MPL-2.0) AND Unlicense AND (MIT OR GPL-3.0) AND (MIT OR GPL-2.0) AND (Apache-2.0 OR ps-list (5.0.1) * License: MIT * Project: https://github.com/sindresorhus/ps-list * Source: https://github.com/sindresorhus/ps-list read-pkg (4.0.1) * License: MIT * Project: https://github.com/sindresorhus/read-pkg * Source: https://github.com/sindresorhus/read-pkg regular expressions and helper function copied from microsoft/vscode (1.33.1) * License: MIT requestretry (3.1.0) * License: MIT * Project: https://github.com/FGRibreau/node-request-retry * Source: https://github.com/FGRibreau/node-request-retry rimraf (2.6.2) * License: ISC textmate/tcl.tmbundle (n/a) * License: LicenseRef-Php_Tmbundle theia npm node (n/a) * License: BSD-2-Clause OR (MIT OR Apache-2.0) AND (AFL-2.1 OR BSD-3-Clause) AND Apache-2.0 AND Artistic-2.0 AND BSD-3-Clause AND (BSD-3-Clause OR MIT) AND MPL-2.0 AND CC0-1.0 AND CC-BY-3.0 AND CC-BY-4.0 AND CC-BY-SA-2.5 AND GPL-2.0 WITH Autoconf-ex theia-cpp-extension npm node (n/a) * License: BSD-2-Clause OR (MIT OR Apache-2.0) AND MIT AND BSD-3-Clause AND Zlib AND (MIT OR GPL-3.0) AND OFL-1.1 AND Apache-2.0 AND CC0-1.0 AND CC-BY-3.0 AND ISC AND MPL-2.0 AND License-Ref-Public-Domain AND BSL-1.0 AND (AFL-2.1 OR BSD-3.0) AND Unlicense AND Artist tslint (5.10.0) * License: Apache-2.0 AND MIT * Project: http://palantir.github.io/tslint/ * Source: https://github.com/palantir/tslint typefox/monaco-language-client (0.5.0) * License: MIT typescript-formatter (7.2.2) * License: MIT * Project: https://github.com/vvakame/typescript-formatter * Source: https://github.com/vvakame/typescript-formatter VS Code (1.33.0) * License: MIT vscode (1.26.0) * License: MIT AND LicenseRef-Php_Tmbundle vscode (1.26.0) * License: MIT vscode (1.31.0) * License: MIT vscode-debugadapter-node (n/a) * License: MIT vscode-java (0.36.0) * License: EPL-1.0 vscode-java (0.44.0) * License: EPL-1.0 vscode-java-debug (0.15.0) * License: MIT when (3.7.8) * License: MIT * Project: https://github.com/cujojs/when * Source: https://github.com/cujojs/when wjordan/browser-path SHA6719d19077b1454bff8b802f9be79cb1b69ebe7e (n/a) * License: MIT xterm.js (3.9.1) * License: MIT * Project: https://xtermjs.org/ * Source: https://github.com/xtermjs/xterm.js yargs (12.0.1) * License: MIT * Project: http://yargs.js.org/ * Source: https://github.com/yargs/yargs yeoman environment (2.3.0) * License: BSD-2-Clause AND BSD-3-Clause * Project: https://github.com/yeoman/environment * Source: https://github.com/yeoman/environment yeoman generator (3.0.0) * License: BSD-2-Clause AND BSD-3-Clause * Project: http://yeoman.io * Source: https://github.com/yeoman/generator yeoman-generator (2.0) * License: BSD-2-Clause * Project: http://yeoman.io/ * Source: https://github.com/yeoman/generator yosay (2.0.2) * License: BSD-2-Clause * Project: https://github.com/yeoman/yosay * Source: https://github.com/yeoman/yosay ## Cryptography Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. ## Electron NOTICE: Please note Electron combines Chromium and Node.js into a single runtime. While Electron, Chromium and Node.js are generally licensed under very permissive MIT and BSD-3-Clause licenses, both Electron and Chromium distribute FFmpeg. While FFmpeg is under the LGPL-2.1-or-later license it incorporates several optional parts and optimizations that are covered by the GPL-2.0-or-later. We understand both Electron and Chromium do not distribute versions of FFmpeg with GPL content enabled; however, FFmpeg may be configured enabled to work with proprietary codecs. It is our understanding these proprietary codecs may be patented; and as a result, may be subject to licensing fees. We strongly recommend downstream consumers verify the type of FFmpeg support configured and modify as required. More information on instructions to verify can be found here https://electronjs.org/docs/development/upgrading-chromium#verify-ffmpeg-support ================================================ FILE: PUBLISHING.md ================================================ # Publishing Guide for the Eclipse Theia IDE This document provides a unified, structured guide for publishing a new version of the Theia IDE. It covers everything from updating package versions, preview testing, releasing, promoting to stable, and other post-release activities. ## Table of Contents 1. [Overview](#1-overview) 2. [Update Package Versions and Theia](#2-update-package-versions-and-theia) - [2.1 Install build dependencies](#21-install-build-dependencies) - [2.2 Update versions](#22-update-versions) - [2.3 Check for mandatory code changes](#23-check-for-mandatory-code-changes) - [2.4 Prepare Release PR](#24-prepare-release-pr) - [2.5 Mac Artifacts](#25-mac-artifacts) - [2.6 Merge Release PR & Trigger Jenkins Build](#26-merge-release-pr--trigger-jenkins-build) 3. [Preview, Testing, and Release Process](#3-preview-testing-and-release-process) - [3.1 Confirm the new preview version is published](#31-confirm-the-new-preview-version-is-published-do-not-promote-as-stable-yet) - [3.2 Announce Preview Test Phase](#32-announce-preview-test-phase) - [3.3 Patch Releases](#33-patch-releases) 4. [Promote IDE from Preview to Stable Channel](#4-promote-ide-from-preview-to-stable-channel) 5. [Tag the Release Commit](#5-tag-the-release-commit) 6. [Publish Docker Image](#6-publish-docker-image) 7. [Snap Update](#7-snap-update) 8. [Upgrade Dependencies](#8-upgrade-dependencies) ## 1. Overview Every commit to the master branch is automatically published as a preview version. Official updates require a version change. This guide differentiates between two version numbers: - **THEIA_VERSION**: (used variable: {{version}}) The version of Theia used in this release. - **THEIA_IDE_VERSION**: (used variable: {{ideVersion}}) The Theia IDE release version. Depending on the context: - If there was **no** Theia release, increment the patch version by 1 (e.g., 1.47.0 -> 1.47.1 or 1.47.100 -> 1.47.101). - For a new Theia *minor* release (e.g., 1.48.0), use the same version as Theia. - For a new Theia *patch* release (e.g., 1.48.1), use Theia's patch version multiplied by 100 (e.g., 1.48.100). ## 2. Update Package Versions and Theia Follow these steps to update dependencies and package versions: ### 2.1. Install build dependencies ```sh yarn ``` ### 2.2. Update versions 1. Update the monorepo version to **THEIA_IDE_VERSION** (without creating a Git tag): ```sh yarn version --no-git-tag-version --new-version {{ideVersion}} ``` 2. Optional: If there is a new Theia release to consume, update Theia dependencies to **THEIA_VERSION** : ```sh yarn update:theia {{version}} && yarn update:theia:children {{version}} ``` 3. Update all package versions to **THEIA_IDE_VERSION** (select in input prompt): ```sh yarn lerna version --exact --no-push --no-git-tag-version ``` 4. Update the yarn lock file: ```sh yarn ``` ### 2.3. Check for mandatory code changes Update the code to include everything that should be part of the release: - Implement all tickets that are located in: - If there was a Theia release: - Review breaking changes - Check for new built-ins - Check sample applications changes - Update code as necessary - Check for new theia packages to be consumed in the example applications (in case there are no tickets) ### 2.4. Prepare Release PR After completing step 2.3, open a PR with your changes PR Title: ```md Update to Theia v{{version}} ``` OR (if it is a pure Theia IDE version update): ```md Publish Theia IDE {{ideVersion}} ``` ### 2.5. Mac Artifacts - The PR will trigger a verification build that generates two zip files with mac artifacts. - Download these two zips and replace them in this pre-release: . - These unsigned dmgs will be used as input for the Jenkins build. ### 2.6. Merge Release PR & Trigger Jenkins Build 1. Merge PR 2. ==> Steps 2.4 and 2.5 need to be complete to proceed! 3. Once [CI checks after merge to master are complete](https://github.com/eclipse-theia/theia-ide/actions), trigger the Jenkins Release Preview job without parameters. 4. Once 3. is successful the notarize job is started automatically. 5. Once 4. is successful it starts the upload job *Note*: Please report if upload fails more than 5 times, we need to investigate! ## 3. Preview, Testing, and Release Process Once the PR is merged and the preview build is created, follow these steps for testing and eventual release: ### 3.1 Confirm the new preview version is published (do not promote as stable yet) - Check if uploaded versions are complete in in download folder - e.g. check if the latest files are correct and the artifacts are there (in doubt, compare to previous versions) ### 3.2 Announce Preview Test Phase ### 3.2.1 GH Discussions - Use GitHub Discussions for the announcement in the [Category Release Announcements](https://github.com/eclipse-theia/theia/discussions/new?category=release-announcements). Title: ```md Theia IDE {{majorMinor}}.x Preview Testing ``` Body: ```md The new version {{ideVersion}} of the Theia IDE is available on the preview channel now. Please join the preview testing! You can download it here: - [Linux](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/linux/TheiaIDE.AppImage) - [Mac x86](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/macos/TheiaIDE.dmg) - [Mac ARM](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/macos-arm/TheiaIDE.dmg) - [Windows](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/windows/TheiaIDESetup.exe) Update your existing installation by setting the preference `updates.channel` to `preview`. Please respond here when you can test the preview without finding blockers, by commenting with a ✔️. If you find any issues, please mention them in this thread and report them as an issue once confirmed by other testers. | Phase | Target | Status | |:---|:---|:---| | {{ideVersion}} preview available | {{releaseDate}} | :white_check_mark: | | {{ideVersion}} community preview window | {{previewStart}} → {{previewEnd}} | :hourglass_flowing_sand: | | Theia IDE {{majorMinor}}.x Promoted to Stable | {{previewEnd}} + 1 business day | | | Docker image Publish | {{previewEnd}} + 1 business day | | | Snap updated | {{previewEnd}} + 1 business day | | ``` - Pin discussion to the Release Announcement Category ### 3.2.2 theia-dev mailing list - Announce the preview release via email to [the `theia-dev` mailing List](mailto:theia-dev@eclipse.org) with the following template: Subject: ```md Theia IDE {{majorMinor}}.x preview phase ``` Body: ```md Hi everyone, Version {{ideVersion}} of the Theia IDE is now available on the preview channel. Please join the preview test and help us stabilize the release. Visit the preview discussion for more information and coordination: https://github.com/eclipse-theia/theia/discussions/{{discussionNumber}} ``` ### 3.2.3 Eclipse Theia Release discussion - Announce the start of the Theia IDE Preview Test phase in the Theia Release announcement (`Eclipse Theia v{{version}}`) discussion (see ): ```md The preview test phase for the Theia IDE {{ideVersion}} has started. You can find the details here: - https://github.com/eclipse-theia/theia/discussions/{{discussionNumber}} ``` ### 3.2.4 Optional: Announcement to Theia IDE Preview Testers - Optional: Announce the start of the Theia IDE Preview Test phase to your testers (e.g., via Slack, Teams, E-Mail): ```md :theia: The Theia IDE preview {{ideVersion}} for Theia version {{version}} is now available! Please take a moment to test it and provide feedback - whether you've run into issues or everything works as expected. {{linkToNewPreviewComment}} To help us get a quick overview, please react with the emoji for your OS (:ubuntu:, :windows:, :mac_arm:, :mac_x64:) once you updated to the new version. Thanks! ``` ### 3.3 Patch Releases - Address reported blockers and issue patch releases (this process may take 1–2 weeks). **Note:** If issues are persistent, or resources are insufficient, the release may be postponed to the next version. - If a blocker was found add this status to the respective preview window: ```md :no_entry: blocker was found ``` ### 3.3.1 Update Preview Discussion with new preview - For Patch Releases, use the [Preview discussion](#32-announce-preview-test-phase) and post a comment to announce the patch release of the Theia IDE: ```md The new version {{ideVersion}} of the Theia IDE is available on the preview channel now. Please join the preview testing! You can download it here: - [Linux](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/linux/TheiaIDE.AppImage) - [Mac x86](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/macos/TheiaIDE.dmg) - [Mac ARM](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/macos-arm/TheiaIDE.dmg) - [Windows](https://download.eclipse.org/theia/ide-preview/{{ideVersion}}/windows/TheiaIDESetup.exe) Update your existing installation by setting the preference `updates.channel` to `preview`. Please respond here when you can test the preview without finding blockers, by commenting with a ✔️. If you find any issues, please mention them in this thread and report them as an issue once confirmed by other testers. ``` ### 3.3.2 Update Preview Discussion Table - Adapt the target dates to the planned new preview window and final release dates - Add comments in the status if necessary - See example announcements here: - Update the discussion's status table with two rows for the current patch release: ```md | [{{ideVersion}} preview available]({{linkToNewPreviewComment}}) | {{previewStart}} | :white_check_mark: | | {{ideVersion}} community preview window | {{previewStart}} → {{previewEnd}} | :hourglass_flowing_sand: | ``` - Also adapt the target dates of the remaining entries in table (if not yet promoted) ### 3.3.3 Optional: Internal Slack announcement - Optional: Announce the start of the Theia IDE Preview Test phase in your internal Slack channel: ```md :theia: The Theia IDE preview {{ideVersion}} for Theia version {{version}} is now available! Please take a moment to test it and provide feedback - whether you've run into issues or everything works as expected. {{linkToNewPreviewComment}} To help us get a quick overview, please react with the emoji for your OS (:ubuntu:, :windows:, :mac_arm:, :mac_x64:) once you updated to the new version. Thanks! ``` ## 3.4 Optional: Internal Slack - feedback reminder - Optional: Schedule a slack reminder close to the preview end: ```md Quick reminder: We're planning to promote the new IDE version (e.g., tomorrow/on Monday), please share your feedback for the preview {{ideVersion}}! :point_right: https://github.com/eclipse-theia/theia/discussions/{{discussionNumber}} Thanks! :thx: ``` ## 4. Promote IDE from Preview to Stable Channel Promote the IDE using the [Build Job](https://ci.eclipse.org/theia/job/Theia%20-%20Promote%20IDE/). - Specify the release version in the `VERSION` parameter (e.g., 1.48.0), corresponding to the **THEIA_IDE_VERSION** copied from . - Post a comment to announce the official release of the Theia IDE: ```md {{ideVersion}} has been promoted to stable ``` - Update the [Base Preview discussion](#32-announce-preview-test-phase) status table with a checkmark and the version that has been published. ```md | [Theia IDE {{majorMinor}}.x Promoted to Stable](https://download.eclipse.org/theia/ide/1.67.100/) | {{today}} | :white_check_mark: | ``` - Mark the message as the answer. - Unpin discussion from the Release Announcement Category. ## 5. Tag the Release Commit After promoting the release, tag the release commit as follows: 1. Create the tag: ```bash git tag v{{ideVersion}} ${SHA of release commit} ``` 2. Push the tag to the repository: ```bash git push origin v{{ideVersion}} ``` ## 6. Publish Docker Image Publish the Docker image by running the [workflow](https://github.com/eclipse-theia/theia-ide/actions/workflows/publish-theia-ide-img.yml) from the `master` branch. Use **${THEIA_IDE_VERSION}** as the version. (We do NOT use the v prefix here in this case currently). - Check the GH package page if the image was published correctly: - Update the [Preview discussion](#32-announce-preview-test-phase) status table ```md | [Docker image Publish](https://github.com/eclipse-theia/theia-ide/pkgs/container/theia-ide%2Ftheia-ide) | {{today}} | :white_check_mark: | ``` ## 7. Snap Update Can be parallel to step 6. After the IDE is promoted to stable, perform these steps for the snap update: 1. Run [this workflow](https://github.com/eclipse-theia/theia-ide-snap/actions/workflows/update.yml) from the `master` branch. 2. After the build succeeded, visit to find the PR that updates to **${THEIA_IDE_VERSION}**. 3. Check out the corresponding branch. 4. Amend the latest commit with your author details: ```bash git commit --amend --author="Your Name " ``` 5. Force push the branch. 6. Verify that all checks pass, and then `rebase and merge`. 7. Confirm the master branch build (Store Publishing) is successful. 8. Check if snap is available 9. Update the [Preview discussion](#32-announce-preview-test-phase) status table ```md | [Snap updated](https://snapcraft.io/theia-ide) | {{today}} | :white_check_mark: | ``` ## 8. Upgrade Dependencies After each release, run the following command to upgrade dependencies: Keep this upgrade process in a separate PR, as it might require IP Reviews from the Eclipse Foundation and additional time. Also, verify the `electron` version in `yarn.lock` and adjust `electronVersion` in `applications/electron/electron-builder.yml` if it has changed. To perform the upgrade: - Run `yarn upgrade` at the root of the repository. - Fix any compilation errors, typing errors, and failing tests. - Open a PR with the changes ([example](https://github.com/eclipse-theia/theia-ide/pull/568)). - The license check review is done via the CI. - Wait for the "IP Check" to complete ([example](https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/22828)). Performing this after the release helps us to find issues with the new dependencies and gives time to perform a license check on the dependencies. ================================================ FILE: README.md ================================================
The Eclipse Theia IDE is built with this project.\ Eclipse Theia IDE also serves as a template for building desktop-based products based on the Eclipse Theia platform.
[![Installers](https://img.shields.io/badge/download-installers-blue.svg?style=flat-curved)](https://theia-ide.org//#theiaidedownload) [![Build Status](https://ci.eclipse.org/theia/buildStatus/icon?subject=latest&job=Theia2%2Fmaster)](https://ci.eclipse.org/theia/job/Theia2/job/master/) [Main Theia Repository](https://github.com/eclipse-theia/theia) [Visit the Theia website](http://www.theia-ide.org) for more documentation: [Using the Theia IDE](https://theia-ide.org/docs/user_getting_started/), [Packaging Theia as a Desktop Product](https://theia-ide.org/docs/blueprint_documentation/). ## License - [MIT](LICENSE) ## Trademark "Theia" is a trademark of the Eclipse Foundation ## What is this? The Eclipse IDE is a modern and open IDE for cloud and desktop. The Theia IDE is based on the [Theia platform](https://theia-ide.org). The Theia IDE is available as a [downloadable desktop application](https://theia-ide.org//#theiaidedownload). You can also try the latest version of the Theia IDE online. The online test version is limited to 30 minutes per session and hosted via Theia.cloud. Finally, we provide an [experimental Docker image](#docker) for hosting the Theia IDE online. The Eclipse Theia IDE also serves as a **template** for building desktop-based products based on the Eclipse Theia platform, as well as to showcase Eclipse Theia capabilities. It is made up of a subset of existing Eclipse Theia features and extensions. [Documentation is available](https://theia-ide.org/docs/composing_applications/) to help you customize and build your own Eclipse Theia-based product. ## Theia IDE vs Theia Blueprint The Theia IDE has been rebranded from its original name “Theia Blueprint”. You can therefore assume the terms “Theia IDE” and “Theia Blueprint” to be synonymous. ## Development ### Requirements Please check Theia's [prerequisites](https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md#prerequisites), and keep node versions aligned between Theia IDE and that of the referenced Theia version. ### Documentation Documentation on how to package Theia as a Desktop Product may be found [here](https://theia-ide.org/docs/blueprint_documentation/) For adopters building their own products based on this template, see the [Adopter Guide](ADOPTER.md) for additional considerations. ### Repository Structure - Root level configures mono-repo build with lerna - `applications` groups the different app targets - `browser` contains a browser based version of Eclipse Theia IDE that may be packaged as a Docker image - `electron` contains the electron app to package, packaging configuration, and E2E tests for the electron target. - `theia-extensions` groups the various custom theia extensions for the Eclipse Theia IDE - `product` contains a Theia extension contributing the product branding (about dialogue and welcome page). - `updater` contains a Theia extension contributing the update mechanism and corresponding UI elements (based on the electron updater). - `launcher` contains a Theia extension contributing, for AppImage applications, the option to create a script that allows to start the Eclipse Theia IDE from the command line by calling the 'theia' command. - `patches` contains patches applied to upstream packages ### Build For development and casual testing of the Eclipse Theia IDE, one can build it in "dev" mode. This permits building the IDE on systems with less resources, like a Raspberry Pi 4B with 4GB of RAM. NOTE: If manually building after updating dependencies or pulling to a newer commit, run `git clean -xfd` to help avoid runtime conflicts. ```sh # Build "dev" version of the app. Its quicker, uses less resources, # but the front end app is not "minified" yarn && yarn build:dev && yarn download:plugins ``` Production applications: ```sh # Build production version of the Eclipse Theia IDE app yarn && yarn build && yarn download:plugins ``` ### Package the Applications ATM we only produce packages for the Electron application. _If you are trying to compile for arm on an arm machine, you may want to follow [these steps](https://github.com/eclipse-theia/theia-ide/issues/690#issuecomment-4157768849) before_ ```sh yarn package:applications # or yarn electron package ``` The packaged application is located in `applications/electron/dist`. ### Create a Preview Electron Electron Application (without packaging it) ```sh yarn electron package:preview ``` The packaged application is located in `applications/electron/dist`. ### Running E2E Tests on Electron The E2E tests basic UI tests of the actual application. This is done based on the preview of the packaged application. ```sh yarn electron package:preview yarn electron test ``` ### Running Browser app The browser app may be started with ```sh yarn browser start ``` and connect to ### Developing with Local Theia Framework To build and test the Theia IDE against a local development version of the Theia framework, see [docs/developing-with-local-theia.md](docs/developing-with-local-theia.md). ### Troubleshooting - [_"Don't expect that you can build app for all platforms on one platform."_](https://www.electron.build/multi-platform-build) ### Reporting Feature Requests and Bugs The features in the Eclipse Theia IDE are based on Theia and the included extensions/plugins. For bugs in Theia please consider opening an issue in the [Theia project on Github](https://github.com/eclipse-theia/theia/issues/new/choose). The Eclipse Theia IDE only packages existing functionality into a product and installers for the product. If you believe there is a mistake in packaging, something needs to be added to the packaging or the installers do not work properly, please [open an issue on Github](https://github.com/eclipse-theia/theia-ide/issues/new/choose) to let us know. ### Docker The Docker image of the Theia IDE is currently in _experimental state_. It is built from the same sources and packages as the desktop version, but it is not part of the [preview test](https://github.com/eclipse-theia/theia-ide/blob/master/PUBLISHING.md#preview-testing-and-release-process-for-the-theia-ide). You can find a prebuilt Docker image of the IDE [here](https://github.com/eclipse-theia/theia-ide/pkgs/container/theia-ide%2Ftheia-ide). You can also create the Docker image for the Eclipse Theia IDE based on the browser app with the following build command: ```sh docker build -t theia-ide -f browser.Dockerfile . ``` You may then run this with ```sh docker run -p=3000:3000 --rm theia-ide ``` and connect to ================================================ FILE: applications/browser/package.json ================================================ { "private": true, "name": "theia-ide-browser-app", "description": "Eclipse Theia IDE browser product", "productName": "Theia IDE", "version": "1.71.100", "license": "MIT", "author": "Eclipse Theia ", "homepage": "https://github.com/eclipse-theia/theia-ide#readme", "bugs": { "url": "https://github.com/eclipse-theia/theia/issues" }, "repository": { "type": "git", "url": "git+https://github.com/eclipse-theia/theia-ide.git" }, "engines": { "yarn": ">=1.7.0 <2", "node": ">=22" }, "theia": { "frontend": { "config": { "applicationName": "Theia IDE", "warnOnPotentiallyInsecureHostPattern": false, "preferences": { "toolbar.showToolbar": true, "files.enableTrash": false, "security.workspace.trust.enabled": false }, "reloadOnReconnect": true } }, "backend": { "config": { "warnOnPotentiallyInsecureHostPattern": false, "startupTimeout": -1, "resolveSystemPlugins": false, "configurationFolder": ".theia-ide", "frontendConnectionTimeout": 3000 } }, "generator": { "config": { "preloadTemplate": "./resources/preload.html" } } }, "dependencies": { "@theia/ai-anthropic": "1.72.0-next.20", "@theia/ai-chat": "1.72.0-next.20", "@theia/ai-chat-ui": "1.72.0-next.20", "@theia/ai-claude-code": "1.72.0-next.20", "@theia/ai-code-completion": "1.72.0-next.20", "@theia/ai-codex": "1.72.0-next.20", "@theia/ai-copilot": "1.72.0-next.20", "@theia/ai-core": "1.72.0-next.20", "@theia/ai-core-ui": "1.72.0-next.20", "@theia/ai-editor": "1.72.0-next.20", "@theia/ai-google": "1.72.0-next.20", "@theia/ai-history": "1.72.0-next.20", "@theia/ai-huggingface": "1.72.0-next.20", "@theia/ai-ide": "1.72.0-next.20", "@theia/ai-llamafile": "1.72.0-next.20", "@theia/ai-mcp": "1.72.0-next.20", "@theia/ai-mcp-server": "1.72.0-next.20", "@theia/ai-mcp-ui": "1.72.0-next.20", "@theia/ai-ollama": "1.72.0-next.20", "@theia/ai-openai": "1.72.0-next.20", "@theia/ai-scanoss": "1.72.0-next.20", "@theia/ai-terminal": "1.72.0-next.20", "@theia/ai-vercel-ai": "1.72.0-next.20", "@theia/bulk-edit": "1.72.0-next.20", "@theia/callhierarchy": "1.72.0-next.20", "@theia/collaboration": "1.72.0-next.20", "@theia/console": "1.72.0-next.20", "@theia/core": "1.72.0-next.20", "@theia/debug": "1.72.0-next.20", "@theia/dev-container": "1.72.0-next.20", "@theia/editor": "1.72.0-next.20", "@theia/editor-preview": "1.72.0-next.20", "@theia/external-terminal": "1.72.0-next.20", "@theia/file-search": "1.72.0-next.20", "@theia/filesystem": "1.72.0-next.20", "@theia/getting-started": "1.72.0-next.20", "@theia/keymaps": "1.72.0-next.20", "@theia/markers": "1.72.0-next.20", "@theia/memory-inspector": "1.72.0-next.20", "@theia/messages": "1.72.0-next.20", "@theia/metrics": "1.72.0-next.20", "@theia/mini-browser": "1.72.0-next.20", "@theia/monaco": "1.72.0-next.20", "@theia/navigator": "1.72.0-next.20", "@theia/notebook": "1.72.0-next.20", "@theia/outline-view": "1.72.0-next.20", "@theia/output": "1.72.0-next.20", "@theia/plugin-dev": "1.72.0-next.20", "@theia/plugin-ext": "1.72.0-next.20", "@theia/plugin-ext-vscode": "1.72.0-next.20", "@theia/preferences": "1.72.0-next.20", "@theia/preview": "1.72.0-next.20", "@theia/process": "1.72.0-next.20", "@theia/property-view": "1.72.0-next.20", "@theia/remote": "1.72.0-next.20", "@theia/scanoss": "1.72.0-next.20", "@theia/scm": "1.72.0-next.20", "@theia/search-in-workspace": "1.72.0-next.20", "@theia/secondary-window": "1.72.0-next.20", "@theia/task": "1.72.0-next.20", "@theia/terminal": "1.72.0-next.20", "@theia/terminal-manager": "1.72.0-next.20", "@theia/test": "1.72.0-next.20", "@theia/timeline": "1.72.0-next.20", "@theia/toolbar": "1.72.0-next.20", "@theia/typehierarchy": "1.72.0-next.20", "@theia/userstorage": "1.72.0-next.20", "@theia/variable-resolver": "1.72.0-next.20", "@theia/vsx-registry": "1.72.0-next.20", "@theia/workspace": "1.72.0-next.20", "fs-extra": "^9.1.0", "theia-ide-product-ext": "1.71.100" }, "devDependencies": { "@theia/cli": "1.72.0-next.20", "@theia/bundle-plugin": "1.72.0-next.20" }, "scripts": { "clean": "theia clean && rimraf node_modules", "build": "yarn -s rebuild && theia build --app-target=\"browser\" --mode development", "build:prod": "yarn -s rebuild && theia build --app-target=\"browser\"", "rebuild": "theia rebuild:browser --cacheRoot ../..", "start": "theia start --plugins=local-dir:../../plugins", "watch": "concurrently --kill-others -n tsc,build -c red,yellow \"tsc -b -w --preserveWatchOutput\" \"yarn -s watch:bundle\"", "update:theia": "ts-node ../../scripts/update-theia-version.ts", "update:next": "ts-node ../../scripts/update-theia-version.ts next" } } ================================================ FILE: applications/browser/resources/preload.html ================================================
================================================ FILE: applications/browser/tsconfig.json ================================================ { "extends": "../../configs/base.tsconfig", "include": [], "compilerOptions": { "composite": true }, "references": [ { "path": "../../theia-extensions/launcher" }, { "path": "../../theia-extensions/product" } ] } ================================================ FILE: applications/browser/webpack.config.js ================================================ /** * This file can be edited to customize webpack configuration. * To reset delete this file and rerun theia build again. */ // @ts-check const configs = require('./gen-webpack.config.js'); const nodeConfig = require('./gen-webpack.node.config.js'); const path = require('path'); const CopyWebpackPlugin = require('copy-webpack-plugin'); /** * Expose bundled modules on window.theia.moduleName namespace, e.g. * window['theia']['@theia/core/lib/common/uri']. * Such syntax can be used by external code, for instance, for testing. configs[0].module.rules.push({ test: /\.js$/, loader: require.resolve('@theia/application-manager/lib/expose-loader') }); */ // serve favico from root // @ts-ignore configs[0].plugins.push( // @ts-ignore new CopyWebpackPlugin({ patterns: [ { context: path.resolve('.', '..', '..', 'applications', 'browser', 'ico'), from: '**' } ] }) ); module.exports = [ ...configs, nodeConfig.config ]; ================================================ FILE: applications/electron/.eslintrc.js ================================================ /** @type {import('eslint').Linter.Config} */ module.exports = { extends: [ '../../configs/build.eslintrc.json' ], parserOptions: { tsconfigRootDir: __dirname, project: 'tsconfig.eslint.json' } }; ================================================ FILE: applications/electron/electron-builder.yml ================================================ appId: eclipse.theia productName: TheiaIDE copyright: Copyright © 2020-2025 Eclipse Foundation, Inc electronDist: ../../node_modules/electron/dist electronVersion: 39.8.7 asar: true asarUnpack: - "**/lib/backend/native/**" - "**/lib/backend/shell-integrations/**" - "**/lib/build/Release/**" - "**/lib/prebuilds/**" nodeGypRebuild: false npmRebuild: false directories: buildResources: resources # node_modules and package.json are copied automatically # Exclude node_modules manually because electron is copied by electron-builder and we are using a bundled backend files: - src-gen - lib - resources/icons/WindowIcon/512-512.png - resources/TheiaIDESplash.svg - scripts - "!**node_modules/**" extraResources: - from: ../../plugins to: app/plugins win: icon: resources/icons/WindowsLauncherIcons/TheiaIDE.ico target: - nsis publish: provider: generic url: "https://download.eclipse.org/theia/ide/${version}/windows" useMultipleRangeRequest: false mac: icon: resources/icons/MacLauncherIcons/icon.icns category: public.app-category.developer-tools protocols: - name: theia schemes: - theia darkModeSupport: true target: - dmg - zip publish: provider: generic url: "https://download.eclipse.org/theia/ide/latest/macos" linux: icon: resources/icons/LinuxLauncherIcons category: Development mimeTypes: - inode/directory vendor: Eclipse Foundation, Inc target: - deb - AppImage publish: provider: generic url: "https://download.eclipse.org/theia/ide/latest/linux" nsis: menuCategory: true oneClick: false perMachine: false installerHeaderIcon: resources/icons/WindowsLauncherIcons/TheiaIDE.ico installerIcon: resources/icons/WindowsLauncherIcons/TheiaIDE.ico uninstallerIcon: resources/icons/WindowsLauncherIcons/TheiaIDE.ico installerSidebar: resources/icons/InstallerSidebarImage/164-314Windows.bmp uninstallerSidebar: resources/icons/InstallerSidebarImage/164-314Windows.bmp allowToChangeInstallationDirectory: true runAfterFinish: false artifactName: ${productName}Setup.${ext} license: LICENSE dmg: artifactName: ${productName}.${ext} deb: artifactName: ${productName}.${ext} appImage: artifactName: ${productName}.${ext} afterPack: ./scripts/after-pack.js ================================================ FILE: applications/electron/entitlements.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables com.apple.security.cs.disable-library-validation ================================================ FILE: applications/electron/package.json ================================================ { "private": true, "name": "theia-ide-electron-app", "description": "Eclipse Theia IDE product", "productName": "Theia IDE", "version": "1.71.100", "main": "scripts/theia-electron-main.js", "license": "MIT", "author": "Eclipse Theia ", "homepage": "https://github.com/eclipse-theia/theia-ide#readme", "bugs": { "url": "https://github.com/eclipse-theia/theia/issues" }, "repository": { "type": "git", "url": "git+https://github.com/eclipse-theia/theia-ide.git" }, "engines": { "yarn": ">=1.7.0 <2", "node": ">=22" }, "theia": { "target": "electron", "frontend": { "config": { "applicationName": "Theia IDE", "reloadOnReconnect": true, "preferences": { "toolbar.showToolbar": true }, "electron": { "showWindowEarly": false, "splashScreenOptions": { "content": "resources/TheiaIDESplash.svg", "height": 276, "width": 446 } } } }, "backend": { "config": { "frontendConnectionTimeout": -1, "startupTimeout": -1, "resolveSystemPlugins": false, "configurationFolder": ".theia-ide" } }, "generator": { "config": { "preloadTemplate": "./resources/preload.html" } } }, "dependencies": { "@theia/ai-anthropic": "1.72.0-next.20", "@theia/ai-chat": "1.72.0-next.20", "@theia/ai-chat-ui": "1.72.0-next.20", "@theia/ai-claude-code": "1.72.0-next.20", "@theia/ai-code-completion": "1.72.0-next.20", "@theia/ai-codex": "1.72.0-next.20", "@theia/ai-copilot": "1.72.0-next.20", "@theia/ai-core": "1.72.0-next.20", "@theia/ai-core-ui": "1.72.0-next.20", "@theia/ai-editor": "1.72.0-next.20", "@theia/ai-google": "1.72.0-next.20", "@theia/ai-history": "1.72.0-next.20", "@theia/ai-huggingface": "1.72.0-next.20", "@theia/ai-ide": "1.72.0-next.20", "@theia/ai-llamafile": "1.72.0-next.20", "@theia/ai-mcp": "1.72.0-next.20", "@theia/ai-mcp-server": "1.72.0-next.20", "@theia/ai-mcp-ui": "1.72.0-next.20", "@theia/ai-ollama": "1.72.0-next.20", "@theia/ai-openai": "1.72.0-next.20", "@theia/ai-scanoss": "1.72.0-next.20", "@theia/ai-terminal": "1.72.0-next.20", "@theia/ai-vercel-ai": "1.72.0-next.20", "@theia/bulk-edit": "1.72.0-next.20", "@theia/callhierarchy": "1.72.0-next.20", "@theia/collaboration": "1.72.0-next.20", "@theia/console": "1.72.0-next.20", "@theia/core": "1.72.0-next.20", "@theia/debug": "1.72.0-next.20", "@theia/dev-container": "1.72.0-next.20", "@theia/editor": "1.72.0-next.20", "@theia/editor-preview": "1.72.0-next.20", "@theia/electron": "1.72.0-next.20", "@theia/external-terminal": "1.72.0-next.20", "@theia/file-search": "1.72.0-next.20", "@theia/filesystem": "1.72.0-next.20", "@theia/getting-started": "1.72.0-next.20", "@theia/keymaps": "1.72.0-next.20", "@theia/markers": "1.72.0-next.20", "@theia/memory-inspector": "1.72.0-next.20", "@theia/messages": "1.72.0-next.20", "@theia/metrics": "1.72.0-next.20", "@theia/mini-browser": "1.72.0-next.20", "@theia/monaco": "1.72.0-next.20", "@theia/navigator": "1.72.0-next.20", "@theia/notebook": "1.72.0-next.20", "@theia/outline-view": "1.72.0-next.20", "@theia/output": "1.72.0-next.20", "@theia/plugin-dev": "1.72.0-next.20", "@theia/plugin-ext": "1.72.0-next.20", "@theia/plugin-ext-vscode": "1.72.0-next.20", "@theia/preferences": "1.72.0-next.20", "@theia/preview": "1.72.0-next.20", "@theia/process": "1.72.0-next.20", "@theia/property-view": "1.72.0-next.20", "@theia/remote": "1.72.0-next.20", "@theia/remote-wsl": "1.72.0-next.20", "@theia/scanoss": "1.72.0-next.20", "@theia/scm": "1.72.0-next.20", "@theia/search-in-workspace": "1.72.0-next.20", "@theia/secondary-window": "1.72.0-next.20", "@theia/task": "1.72.0-next.20", "@theia/terminal": "1.72.0-next.20", "@theia/terminal-manager": "1.72.0-next.20", "@theia/test": "1.72.0-next.20", "@theia/timeline": "1.72.0-next.20", "@theia/toolbar": "1.72.0-next.20", "@theia/typehierarchy": "1.72.0-next.20", "@theia/userstorage": "1.72.0-next.20", "@theia/variable-resolver": "1.72.0-next.20", "@theia/vsx-registry": "1.72.0-next.20", "@theia/workspace": "1.72.0-next.20", "fs-extra": "^9.1.0", "theia-ide-launcher-ext": "1.71.100", "theia-ide-product-ext": "1.71.100", "theia-ide-updater-ext": "1.71.100" }, "devDependencies": { "@theia/cli": "1.72.0-next.20", "@theia/bundle-plugin": "1.72.0-next.20", "@types/js-yaml": "^3.12.10", "@types/yargs": "17.0.7", "@wdio/cli": "^6.12.1", "@wdio/local-runner": "^6.12.1", "@wdio/mocha-framework": "^6.11.0", "@wdio/spec-reporter": "^6.11.0", "app-builder-lib": "26.0.12", "chai": "^4.5.0", "concurrently": "^3.6.1", "electron": "39.8.7", "electron-builder": "26.0.12", "electron-chromedriver": "^28.3.3", "electron-mocha": "^12.3.1", "electron-osx-sign": "^0.6.0", "js-yaml": "^3.14.2", "mocha": "^8.4.0", "rimraf": "^2.7.1", "ts-node": "^10.9.2", "wdio-chromedriver-service": "^6.0.4", "webdriverio": "^6.12.1", "yargs": "17.2.1" }, "scripts": { "clean": "theia clean && rimraf node_modules", "clean:dist": "rimraf dist", "build": "yarn -s rebuild && theia build --app-target=\"electron\" --mode development", "build:prod": "yarn -s rebuild && theia build --app-target=\"electron\"", "rebuild": "theia rebuild:electron --cacheRoot ../..", "watch": "concurrently -n compile,build \"theiaext watch --preserveWatchOutput\" \"theia build --watch --mode development\"", "start": "electron scripts/theia-electron-main.js --plugins=local-dir:../../plugins", "start:debug": "yarn start --log-level=debug", "package": "yarn clean:dist && yarn rebuild && electron-builder -c.mac.identity=null --publish never", "package:prod": "yarn deploy", "deploy": "yarn clean:dist && yarn rebuild && electron-builder -c.mac.identity=null --publish always", "package:preview": "yarn clean:dist && yarn rebuild && electron-builder -c.mac.identity=null --dir", "update:checksum": "ts-node scripts/update-checksum.ts", "update:blockmap": "ts-node scripts/update-blockmap.ts", "update:theia": "ts-node ../../scripts/update-theia-version.ts", "update:next": "ts-node ../../scripts/update-theia-version.ts next", "sign:directory": "ts-node scripts/sign-directory.ts", "sign:directory:windows": "ts-node scripts/sign-directory-windows.ts", "test": "mocha --timeout 60000 \"./test/*.spec.js\"", "lint": "eslint --ext js,jsx,ts,tsx scripts && eslint --ext js,jsx,ts,tsx test", "lint:fix": "eslint --ext js,jsx,ts,tsx scripts --fix && eslint --ext js,jsx,ts,tsx test -fix" } } ================================================ FILE: applications/electron/resources/LICENSE ================================================ MIT License Copyright (c) 2020 Eclipse Theia Blueprint Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: applications/electron/resources/icons/MacLauncherIcons/icon.icon/icon.json ================================================ { "fill" : { "automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000" }, "groups" : [ { "layers" : [ { "image-name" : "icon.png", "name" : "icon", "position" : { "scale" : 1.67, "translation-in-points" : [ 0, 0 ] } } ], "name" : "Icon", "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } }, { "blur-material" : null, "hidden" : false, "layers" : [ { "image-name" : "Theia Light BG.jpg", "name" : "Theia Light BG" } ], "lighting" : "combined", "name" : "Light Mode Content", "opacity-specializations" : [ { "appearance" : "dark", "value" : 0 } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } }, { "hidden" : false, "layers" : [ { "image-name" : "Theia Dark BG.jpg", "name" : "Theia Dark BG" } ], "lighting" : "combined", "name" : "Dark Mode Content", "opacity-specializations" : [ { "value" : 0 }, { "appearance" : "dark", "value" : 1 } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } } ], "supported-platforms" : { "circles" : [ "watchOS" ], "squares" : "shared" } } ================================================ FILE: applications/electron/resources/preload.html ================================================
================================================ FILE: applications/electron/scripts/after-pack.js ================================================ #!/usr/bin/env node const fs = require('fs'); const path = require('path'); const util = require('util'); const child_process = require('child_process'); const rimraf = require('rimraf'); const sign_util = require('electron-osx-sign/util'); const asyncRimraf = util.promisify(rimraf); const DELETE_PATHS = [ 'Contents/Resources/app/node_modules/unzip-stream/aa.zip', 'Contents/Resources/app/node_modules/unzip-stream/testData*' ]; const signCommand = path.join(__dirname, 'sign.sh'); const notarizeCommand = path.join(__dirname, 'notarize.sh'); const entitlements = path.resolve(__dirname, '..', 'entitlements.plist'); const signFile = file => { const stat = fs.lstatSync(file); const mode = stat.isFile() ? stat.mode : undefined; console.log(`Signing ${file}...`); child_process.spawnSync(signCommand, [ path.basename(file), entitlements ], { cwd: path.dirname(file), maxBuffer: 1024 * 10000, env: process.env, stdio: 'inherit', encoding: 'utf-8' }); if (mode) { console.log(`Setting attributes of ${file}...`); fs.chmodSync(file, mode); } }; exports.default = async function (context) { await afterPackHook(context); const running_ci = process.env.THEIA_IDE_JENKINS_CI === 'true'; const releaseDryRun = process.env.THEIA_IDE_JENKINS_RELEASE_DRYRUN === 'true'; const branch = process.env.BRANCH_NAME; const running_on_mac = context.packager.platform.name === 'mac'; const appPath = path.resolve(context.appOutDir, `${context.packager.appInfo.productFilename}.app`); // Remove anything we don't want in the final package for (const deletePath of DELETE_PATHS) { const resolvedPath = path.resolve(appPath, deletePath); console.log(`Deleting ${resolvedPath}...`); await asyncRimraf(resolvedPath); } // Only continue for macOS during CI if ((( branch === 'master' || releaseDryRun) && running_ci && running_on_mac)) { console.log('Detected Theia IDE Release on Mac ' + releaseDryRun ? ' (dry-run)' : '' + ' - proceeding with signing and notarizing'); } else { if (running_on_mac) { console.log('Not a release or dry-run requiring signing/notarizing - skipping'); } return; } // Use app-builder-lib to find all binaries to sign, at this level it will include the final .app let childPaths = await sign_util.walkAsync(context.appOutDir); // Sign deepest first // From https://github.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/electron-osx-sign/sign.js#L120 childPaths = childPaths.sort((a, b) => { const aDepth = a.split(path.sep).length; const bDepth = b.split(path.sep).length; return bDepth - aDepth; }); // Sign binaries childPaths.forEach(file => signFile(file, context.appOutDir)); // Notarize app child_process.spawnSync(notarizeCommand, [ path.basename(appPath), context.packager.appInfo.info._configuration.appId ], { cwd: path.dirname(appPath), maxBuffer: 1024 * 10000, env: process.env, stdio: 'inherit', encoding: 'utf-8' }); }; // taken and modified from: https://github.com/gergof/electron-builder-sandbox-fix/blob/a2251d7d8f22be807d2142da0cf768c78d4cfb0a/lib/index.js const afterPackHook = async params => { if (params.electronPlatformName !== 'linux') { // this fix is only required on linux return; } const executable = path.join( params.appOutDir, params.packager.executableName ); const loaderScript = `#!/usr/bin/env bash set -u SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )" exec "$SCRIPT_DIR/${params.packager.executableName}.bin" "--no-sandbox" "$@" `; try { await fs.promises.rename(executable, executable + '.bin'); await fs.promises.writeFile(executable, loaderScript); await fs.promises.chmod(executable, 0o755); } catch (e) { throw new Error('Failed to create loader for sandbox fix:\n' + e); } }; ================================================ FILE: applications/electron/scripts/appimage-helpers.js ================================================ const fs = require('fs'); const path = require('path'); /** * Reads the plugin copy metadata file and returns its content. * @param metadataPath - Path to the metadata file * @returns The metadata object or undefined if not found */ function readPluginCopyMetadata(metadataPath) { if (!fs.existsSync(metadataPath)) { return undefined; } try { return JSON.parse(fs.readFileSync(metadataPath, 'utf8')); } catch (err) { console.warn('Could not read built-in plugin copy metadata file:', err.message); return undefined; } } /** * Writes the plugin copy metadata file with version and timestamp. * @param metadataPath - Path to the metadata file * @param version - Current version */ function writePluginCopyMetadata(metadataPath, version) { const metadata = { version: version, copiedAt: new Date().toISOString() }; fs.writeFileSync(metadataPath, JSON.stringify(metadata, undefined, 2)); } /** * Copies bundled plugins from AppImage to user directory if needed. * @param bundledPluginsDir - Path to bundled plugins in AppImage * @param userPluginsDir - Path to user built-in plugins directory * @param currentVersion - Current Theia IDE version * @returns true if the builtins were copied to the user dir, false if there was an error */ function copyBundledPlugins(bundledPluginsDir, userPluginsDir, currentVersion) { const metadataFile = path.join(userPluginsDir, '.builtInPlugins-metadata'); // Ensure the user plugins directory exists if (!fs.existsSync(userPluginsDir)) { fs.mkdirSync(userPluginsDir, { recursive: true }); } // Check if built-in plugins need to be copied const metadata = readPluginCopyMetadata(metadataFile); let shouldCopy = false; if (!metadata) { shouldCopy = true; } else if (metadata.version !== currentVersion) { console.log(`Theia IDE updated from ${metadata.version} to ${currentVersion}. Updating built-in plugins...`); shouldCopy = true; } if (!shouldCopy) { console.log('Built-in plugins were already copied.'); return true; } console.log(`Copying bundled plugins from AppImage to ${userPluginsDir}...`); try { // Clean existing plugins directory to remove old/obsolete plugins fs.rmSync(userPluginsDir, { recursive: true, force: true }); fs.mkdirSync(userPluginsDir, { recursive: true }); const pluginEntries = fs.readdirSync(bundledPluginsDir, { withFileTypes: true }); for (const entry of pluginEntries) { const srcPath = path.join(bundledPluginsDir, entry.name); const destPath = path.join(userPluginsDir, entry.name); fs.cpSync(srcPath, destPath, { recursive: true }); } writePluginCopyMetadata(metadataFile, currentVersion); console.log(`Bundled plugins copied successfully to ${userPluginsDir}.`); } catch (err) { console.error('Failed to copy bundled plugins:', err.message); return false; } return true; } module.exports = { copyBundledPlugins, }; ================================================ FILE: applications/electron/scripts/generate-app-update-yml.js ================================================ #!/usr/bin/env node /******************************************************************************** * Copyright (C) 2026 STMicroelectronics and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ // Generates app-update.yml for the Windows auto-updater. // // The normal electron-builder flow generates this file during afterPack, but // only when the target is "nsis" or "appx". The Windows CI build splits // packaging into two steps: // 1. `electron-builder --dir` (target = "dir") → app-update.yml is skipped // 2. `electron-builder --prepackaged` → afterPack does not run // // This script bridges that gap by writing the file before step 2. const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); const electronDir = path.resolve(__dirname, '..'); const pkg = require(path.join(electronDir, 'package.json')); const builderConfig = yaml.load(fs.readFileSync(path.join(electronDir, 'electron-builder.yml'), 'utf8')); const winPublish = builderConfig.win.publish; const version = pkg.version; // Expand ${version} macro in the URL, matching electron-builder's macro expansion const url = winPublish.url.replace('${version}', version); const appUpdateYml = { provider: winPublish.provider, url, ...(winPublish.useMultipleRangeRequest !== undefined && { useMultipleRangeRequest: winPublish.useMultipleRangeRequest }), updaterCacheDirName: `${pkg.name}-updater` }; const outPath = path.join(electronDir, 'dist', 'win-unpacked', 'resources', 'app-update.yml'); fs.writeFileSync(outPath, yaml.dump(appUpdateYml, { lineWidth: -1 })); console.log(`Generated ${outPath}`); console.log(fs.readFileSync(outPath, 'utf8')); ================================================ FILE: applications/electron/scripts/notarize.sh ================================================ #!/bin/bash -x INPUT=$1 APP_ID=$2 NEEDS_UNZIP=false UUID_REGEX='"uuid"\s*:\s*"([^"]+)' STATUS_REGEX='"status"\s*:\s*"([^"]+)' # if folder, zip it if [ -d "${INPUT}" ]; then NEEDS_UNZIP=true zip -r -q -y unsigned.zip "${INPUT}" rm -rf "${INPUT}" INPUT=unsigned.zip fi # copy file to storage server scp -p "${INPUT}" genie.theia@projects-storage.eclipse.org:./ rm -f "${INPUT}" # name to use on server REMOTE_NAME=${INPUT##*/} # notarize over ssh RESPONSE=$(ssh -q genie.theia@projects-storage.eclipse.org curl -X POST -F file=@"\"${REMOTE_NAME}\"" -F "'options={\"primaryBundleId\": \"${APP_ID}\", \"staple\": true};type=application/json'" https://cbi.eclipse.org/macos/xcrun/notarize) # fund uuid and status [[ $RESPONSE =~ $UUID_REGEX ]] UUID=${BASH_REMATCH[1]} [[ $RESPONSE =~ $STATUS_REGEX ]] STATUS=${BASH_REMATCH[1]} # poll progress echo " Progress: $RESPONSE" while [[ $STATUS == 'IN_PROGRESS' ]]; do sleep 120 RESPONSE=$(ssh -q genie.theia@projects-storage.eclipse.org curl -s https://cbi.eclipse.org/macos/xcrun/${UUID}/status) [[ $RESPONSE =~ $STATUS_REGEX ]] STATUS=${BASH_REMATCH[1]} echo " Progress: $RESPONSE" done if [[ $STATUS != 'COMPLETE' ]]; then echo "Notarization failed: $RESPONSE" exit 1 fi # download stapled result ssh -q genie.theia@projects-storage.eclipse.org curl -o "\"stapled-${REMOTE_NAME}\"" https://cbi.eclipse.org/macos/xcrun/${UUID}/download # copy stapled file back from server scp -T -p genie.theia@projects-storage.eclipse.org:"\"./stapled-${REMOTE_NAME}\"" "${INPUT}" # ensure storage server is clean ssh -q genie.theia@projects-storage.eclipse.org rm -f "\"${REMOTE_NAME}\"" "\"stapled-${REMOTE_NAME}\"" entitlements.plist # if unzip needed if [ "$NEEDS_UNZIP" = true ]; then unzip -qq "${INPUT}" if [ $? -ne 0 ]; then # echo contents if unzip failed output=$(cat $INPUT) echo "$output" exit 1 fi rm -f "${INPUT}" fi ================================================ FILE: applications/electron/scripts/sign-directory-windows.ts ================================================ /******************************************************************************** * Copyright (C) 2026 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import child_process from 'child_process'; import fs from 'fs'; import path from 'path'; import { hideBin } from 'yargs/helpers'; import yargs from 'yargs/yargs'; import { formatBytes, replaceWithSigned, walkFiles } from './sign-utils'; /** * Sign Windows binaries inside an electron-builder `--dir` output (`win-unpacked`) by uploading * each `.exe` to the Eclipse Authenticode signing service and writing the signed result back in * place. The traversal must mirror the roots that electron-builder's `winPackager.signApp()` * would sign: the top level of the unpacked directory, `resources/app.asar.unpacked/` and * `swiftshader/`. The filter (`.exe` only) matches electron-builder's default `shouldSignFile` * when `win.signExts` is unset. */ const DEFAULT_SIGN_URL = 'https://cbi.eclipse.org/authenticode/sign'; const argv = yargs(hideBin(process.argv)) .option('directory', { alias: 'd', type: 'string', demandOption: true, description: 'The electron-builder `--dir` output directory (e.g. dist/win-unpacked) containing the unpacked Windows app' }) .option('url', { alias: 'u', type: 'string', default: DEFAULT_SIGN_URL, description: 'The URL of the Authenticode signing service' }) .version(false) .wrap(120) .parseSync(); function isExeFile(filePath: string): boolean { return path.extname(filePath).toLowerCase() === '.exe'; } /** * Collect files to sign by reproducing electron-builder's `winPackager.signApp()` traversal: * 1. Top-level files directly under `/` (no recursion) * 2. Recursive walk of `/resources/app.asar.unpacked/` * 3. Recursive walk of `/swiftshader/` (if present) */ function collectFilesToSign(unpackedDir: string): string[] { const topLevel = walkFiles(unpackedDir, isExeFile, { shallow: true }); const asarUnpacked = walkFiles(path.join(unpackedDir, 'resources', 'app.asar.unpacked'), isExeFile); const swiftshader = walkFiles(path.join(unpackedDir, 'swiftshader'), isExeFile); return [...topLevel, ...asarUnpacked, ...swiftshader]; } function signFile(file: string, url: string): void { const signedPath = `${file}.signed`; // Remove any stale signed file from a previous run if (fs.existsSync(signedPath)) { fs.unlinkSync(signedPath); } const before = fs.statSync(file); console.log(`Signing ${file} (${formatBytes(before.size)})...`); const started = Date.now(); const result = child_process.spawnSync( 'curl', ['-sSf', '-o', signedPath, '-F', `file=@${file}`, url], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' } ); const durationMs = Date.now() - started; if (result.error) { throw new Error(`Failed to invoke curl for ${file}: ${result.error.message}`); } if (result.status !== 0) { // Dump whatever curl produced to help diagnose. If curl wrote a non-binary error payload to // `signedPath` (e.g. an HTML/JSON error page from the signing service), log that too. if (result.stdout) { console.error(`curl stdout:\n${result.stdout}`); } if (result.stderr) { console.error(`curl stderr:\n${result.stderr}`); } if (fs.existsSync(signedPath)) { try { const body = fs.readFileSync(signedPath, 'utf-8'); console.error(`Response body written to ${signedPath}:\n${body}`); } catch { // Ignore read errors on binary/garbage content } try { fs.unlinkSync(signedPath); } catch { // Best-effort cleanup } } throw new Error(`Signing failed for ${file}: curl exited with status ${result.status}`); } if (!fs.existsSync(signedPath)) { throw new Error(`Signing failed for ${file}: no output file produced at ${signedPath}`); } const signedSize = fs.statSync(signedPath).size; if (signedSize === 0) { fs.unlinkSync(signedPath); throw new Error(`Signing failed for ${file}: signed output is empty`); } replaceWithSigned(file, signedPath); console.log(` -> signed in ${durationMs} ms (${formatBytes(signedSize)})`); } function main(): void { const directory = path.resolve(argv.directory); const url = argv.url; if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory()) { console.error(`Directory does not exist or is not a directory: ${directory}`); process.exit(1); } console.log(`sign-directory-windows: directory=${directory}; url=${url}`); const files = collectFilesToSign(directory); if (files.length === 0) { // Having zero files usually means the app layout changed and our walk roots no longer // match what electron-builder would sign. Fail so this is caught in CI rather than // silently shipping an unsigned binary. console.error( `No .exe files found under ${directory} in any of the expected roots (top level, ` + 'resources/app.asar.unpacked, swiftshader). This likely indicates a change in the ' + 'electron-builder output layout that sign-directory-windows has not been updated for.' ); process.exit(1); } console.log(`Found ${files.length} file(s) to sign:`); for (const file of files) { console.log(` - ${path.relative(directory, file)}`); } const overallStarted = Date.now(); for (const file of files) { signFile(file, url); } const overallDuration = Date.now() - overallStarted; console.log(`Signed ${files.length} file(s) in ${overallDuration} ms.`); } main(); ================================================ FILE: applications/electron/scripts/sign-directory.ts ================================================ /******************************************************************************** * Copyright (C) 2025 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { hideBin } from 'yargs/helpers'; import yargs from 'yargs/yargs'; import path from 'path'; import fs from 'fs'; import child_process from 'child_process'; import { walkFiles } from './sign-utils'; const signCommand = path.join(__dirname, 'sign.sh'); const notarizeCommand = path.join(__dirname, 'notarize.sh'); const entitlements = path.resolve(__dirname, '..', 'entitlements.plist'); // File extensions and patterns that need code signing on macOS const BINARY_EXTENSIONS = ['.dylib', '.so', '.node', '.framework']; const BINARY_PATTERNS = [ /^MacOS\//, // Executable files in MacOS directory /^Contents\/MacOS\//, // Executable files in Contents/MacOS directory ]; const EXECUTABLE_NAMES = [ 'node', 'electron', 'rg', 'macos-trash', 'chrome-sandbox' ]; // Function to check if a file is likely a binary that needs signing function isBinaryFile(filePath: string): boolean { const extension = path.extname(filePath); const fileName = path.basename(filePath); const relativePath = filePath.replace(/^.*?\.app\//, ''); // Get path relative to .app bundle // Check by extension if (BINARY_EXTENSIONS.includes(extension)) { return true; } // Check by executable name if (EXECUTABLE_NAMES.includes(fileName)) { return true; } // Check by pattern for (const pattern of BINARY_PATTERNS) { if (pattern.test(relativePath)) { return true; } } // Check if file is executable (Unix-only check) try { const stat = fs.statSync(filePath); if ((stat.mode & 0o111) !== 0) { // Check if execute bit is set // Further verify it's a binary with 'file' command if available try { const fileType = child_process.execSync(`file "${filePath}"`).toString(); return fileType.includes('Mach-O') || fileType.includes('executable') || fileType.includes('shared library') || fileType.includes('dynamically linked'); } catch (e) { // If 'file' command fails, fall back to assuming it's a binary if it has execute permission return true; } } } catch (e) { // If stat fails, skip this check } return false; } // Function to recursively find binaries in a directory function findBinariesToSign(dirPath: string): string[] { const result = walkFiles(dirPath, isBinaryFile, { skipDirs: ['node_modules', '.git'] }); // Sort by path depth (deepest first) to ensure nested binaries are signed first return result.sort((a, b) => { const aDepth = a.split(path.sep).length; const bDepth = b.split(path.sep).length; return bDepth - aDepth; }); } const signFile = (file: string) => { const stat = fs.lstatSync(file); const mode = stat.isFile() ? stat.mode : undefined; // Get SHA hash of file before signing - only for actual files, not directories let shaBeforeSigning: string | undefined; if (stat.isFile()) { shaBeforeSigning = child_process.execSync(`shasum -a 256 "${file}"`).toString().trim(); } console.log(`Signing ${file}...`); child_process.spawnSync(signCommand, [ path.basename(file), entitlements ], { cwd: path.dirname(file), maxBuffer: 1024 * 10000, env: process.env, stdio: 'inherit', encoding: 'utf-8' }); // Get SHA hash of file after signing - only for actual files, not directories if (stat.isFile()) { const shaAfterSigning = child_process.execSync(`shasum -a 256 "${file}"`).toString().trim(); // Log a warning if the SHA hash hasn't changed after signing if (shaBeforeSigning === shaAfterSigning) { console.warn(`WARNING: SHA hash did not change after signing for ${file}. This might indicate the file was not properly signed.`); } } if (mode) { console.log(`Setting attributes of ${file}...`); fs.chmodSync(file, mode); } }; const argv = yargs(hideBin(process.argv)) .option('directory', { alias: 'd', type: 'string', default: 'dist', description: 'The directory which contains the application to be signed' }) .version(false) .wrap(120) .parseSync(); execute(); async function execute(): Promise { console.log(`signCommand: ${signCommand}; notarizeCommand: ${notarizeCommand}; entitlements: ${entitlements}; directory: ${argv.directory}`); // First sign all individual binaries inside the app bundle const binariesToSign = findBinariesToSign(argv.directory); for (const binaryPath of binariesToSign) { signFile(binaryPath); } // Then sign the main app bundle console.log('Signing main application bundle...'); signFile(argv.directory); // Notarize app console.log('Notarizing application...'); child_process.spawnSync(notarizeCommand, [ path.basename(argv.directory), 'eclipse.theia' ], { cwd: path.dirname(argv.directory), maxBuffer: 1024 * 10000, env: process.env, stdio: 'inherit', encoding: 'utf-8' }); } ================================================ FILE: applications/electron/scripts/sign-utils.ts ================================================ /******************************************************************************** * Copyright (C) 2026 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import fs from 'fs'; import path from 'path'; export interface WalkOptions { /** Directory names to skip entirely while recursing (e.g. `['node_modules', '.git']`). */ skipDirs?: string[]; /** If true, do not recurse into subdirectories - only list direct children. Default: false. */ shallow?: boolean; } /** * Recursively walks `root` and returns all file paths for which `filter` returns `true`. * If `root` does not exist, returns an empty array. */ export function walkFiles(root: string, filter: (file: string) => boolean, opts: WalkOptions = {}): string[] { const result: string[] = []; if (!fs.existsSync(root)) { return result; } const skipDirs = new Set(opts.skipDirs ?? []); const visit = (currentPath: string): void => { const entries = fs.readdirSync(currentPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentPath, entry.name); if (entry.isDirectory()) { if (skipDirs.has(entry.name)) { continue; } if (opts.shallow) { continue; } visit(fullPath); } else if (entry.isFile() && filter(fullPath)) { result.push(fullPath); } } }; visit(root); return result; } /** * Replaces `originalPath` with `signedPath` on disk, preserving the file mode of the original. * On POSIX `renameSync` atomically replaces the destination, so no separate unlink is needed. */ export function replaceWithSigned(originalPath: string, signedPath: string): void { const originalMode = fs.statSync(originalPath).mode; fs.renameSync(signedPath, originalPath); fs.chmodSync(originalPath, originalMode); } /** Format a byte count as a human-readable string (e.g. `12.3 MB`). */ export function formatBytes(bytes: number): string { if (!Number.isFinite(bytes) || bytes < 0) { return `${bytes}`; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex++; } return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; } ================================================ FILE: applications/electron/scripts/sign.sh ================================================ #!/bin/bash -x # Enable debug output set -x INPUT=$1 ENTITLEMENTS=$2 NEEDS_UNZIP=false echo "=== DEBUG: Starting signing process for $INPUT ===" # if folder, zip it if [ -d "${INPUT}" ]; then echo "=== DEBUG: Input is a directory, zipping it ===" NEEDS_UNZIP=true zip -r -q -y unsigned.zip "${INPUT}" rm -rf "${INPUT}" INPUT=unsigned.zip fi # copy file to storage server echo "=== DEBUG: Copying $INPUT to storage server ===" scp -p "${INPUT}" genie.theia@projects-storage.eclipse.org:./ if [ $? -eq 0 ]; then echo "=== DEBUG: Successfully copied $INPUT to storage server ===" else echo "=== ERROR: Failed to copy $INPUT to storage server ===" exit 1 fi rm -f "${INPUT}" # copy entitlements to storage server echo "=== DEBUG: Copying entitlements file to storage server ===" scp -p "${ENTITLEMENTS}" genie.theia@projects-storage.eclipse.org:./entitlements.plist if [ $? -eq 0 ]; then echo "=== DEBUG: Successfully copied entitlements to storage server ===" else echo "=== ERROR: Failed to copy entitlements to storage server ===" exit 1 fi # name to use on server REMOTE_NAME=${INPUT##*/} # sign over ssh # https://wiki.eclipse.org/IT_Infrastructure_Doc#Web_service ssh -q genie.theia@projects-storage.eclipse.org curl -f -o "\"signed-${REMOTE_NAME}\"" -F file=@"\"${REMOTE_NAME}\"" -F entitlements=@entitlements.plist https://cbi.eclipse.org/macos/codesign/sign if [ $? -eq 0 ]; then echo "=== DEBUG: Remote signing completed successfully ===" else echo "=== ERROR: Remote signing failed ===" # Try to get error information ssh -q genie.theia@projects-storage.eclipse.org "cat \"signed-${REMOTE_NAME}\" || echo 'No output file found'" exit 1 fi # copy signed file back from server echo "=== DEBUG: Copying signed file back from storage server ===" scp -T -p genie.theia@projects-storage.eclipse.org:"\"./signed-${REMOTE_NAME}\"" "${INPUT}" if [ $? -eq 0 ]; then echo "=== DEBUG: Successfully retrieved signed file ===" else echo "=== ERROR: Failed to retrieve signed file ===" exit 1 fi # Check if the file was actually signed echo "=== DEBUG: Verifying if file was signed properly ===" if [ -f "${INPUT}" ]; then # Get file size to verify it's not empty FILE_SIZE=$(stat -f%z "${INPUT}" 2>/dev/null || stat -c%s "${INPUT}" 2>/dev/null) echo "=== DEBUG: Signed file size: $FILE_SIZE bytes ===" # On macOS, we can verify code signature if [[ "$OSTYPE" == "darwin"* ]]; then echo "=== DEBUG: Checking code signature with codesign -vv ===" codesign -vv "${INPUT}" || echo "=== WARNING: codesign verification failed ===" fi else echo "=== ERROR: Signed file not found ===" exit 1 fi # ensure storage server is clean echo "=== DEBUG: Cleaning up remote files ===" ssh -q genie.theia@projects-storage.eclipse.org rm -f "\"${REMOTE_NAME}\"" "\"signed-${REMOTE_NAME}\"" entitlements.plist echo "=== DEBUG: Remote cleanup completed ===" # if unzip needed if [ "$NEEDS_UNZIP" = true ]; then echo "=== DEBUG: Unzipping signed archive ===" unzip -qq "${INPUT}" if [ $? -ne 0 ]; then # echo contents if unzip failed echo "=== ERROR: Unzip failed, showing file contents ===" output=$(cat $INPUT) echo "$output" exit 1 fi echo "=== DEBUG: Unzip successful, removing zip file ===" rm -f "${INPUT}" # Perform deep codesign check on the directory if running on macOS if [[ "$OSTYPE" == "darwin"* ]]; then echo "=== DEBUG: Performing deep codesign verification on directory ===" # Check if spctl is available (macOS security assessment tool) if command -v spctl &> /dev/null; then # Check if the directory is an app bundle if [[ -d "$1" && "$1" == *.app ]]; then echo "=== DEBUG: Verifying app bundle with spctl --assess --verbose ===" spctl --assess --verbose "$1" || echo "=== WARNING: App bundle verification failed, may not pass notarization ===" fi fi # Find all binary files and check their signatures echo "=== DEBUG: Checking individual binary signatures in $1 ===" find "$1" -type f -exec file {} \; | grep -E "Mach-O|dylib" | cut -d: -f1 | while read binary; do echo "Checking signature for $binary" codesign --verify --deep --strict --verbose=2 "$binary" || echo "=== WARNING: Binary $binary has signature issues, may not pass notarization ===" # Check for hardened runtime codesign -d --verbose=4 "$binary" 2>&1 | grep -q 'Runtime Version=10.0.0' || echo "=== WARNING: Binary $binary may not have hardened runtime enabled ===" done fi fi echo "=== DEBUG: Signing process completed for $1 ===" ================================================ FILE: applications/electron/scripts/theia-electron-main.js ================================================ const path = require('path'); const fs = require('fs'); const os = require('os'); const { copyBundledPlugins } = require('./appimage-helpers'); // Update to override the supported VS Code API version. // process.env.VSCODE_API_VERSION = '1.50.0' // Detect if running as AppImage const isAppImage = !!process.env.APPIMAGE; // When packaged with asar, __dirname is inside app.asar (e.g., .../app.asar/scripts) // but plugins are in extraResources at .../app/plugins (outside the asar) const isInsideAsar = __dirname.includes('.asar'); const bundledPluginsDir = isInsideAsar ? path.join(process.resourcesPath, 'app', 'plugins') : path.resolve(__dirname, '../', 'plugins'); if (isAppImage) { // When running as AppImage, use a user-writable directory for the built-in plugins // The AppImage mount point (/tmp/.mount_*) is read-only const configDir = process.env.THEIA_CONFIG_DIR || path.join(os.homedir(), '.theia-ide'); const userPluginsDir = path.join(configDir, 'builtInPlugins'); const packageJsonPath = path.resolve(__dirname, '../', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const currentVersion = packageJson.version; // Copy bundled plugins to user directory if needed (first run or version update) const useUserDir = copyBundledPlugins(bundledPluginsDir, userPluginsDir, currentVersion); // If copying fails, fall back to the read-only bundled directory (will be improved in follow up of GH-630) process.env.THEIA_DEFAULT_PLUGINS = `local-dir:${useUserDir ? userPluginsDir : bundledPluginsDir}`; } else { // Use a set of builtin plugins in our application. process.env.THEIA_DEFAULT_PLUGINS = `local-dir:${bundledPluginsDir}`; } // Handover to the auto-generated electron application handler. require('../lib/backend/electron-main.js'); ================================================ FILE: applications/electron/scripts/update-blockmap.ts ================================================ /******************************************************************************** * Copyright (C) 2023 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { hideBin } from 'yargs/helpers'; import yargs from 'yargs/yargs'; import { executeAppBuilderAsJson } from 'app-builder-lib/out/util/appBuilder'; // eslint-disable-next-line import/no-extraneous-dependencies import { BlockMapDataHolder } from 'builder-util-runtime'; import { rmSync } from 'fs'; import * as path from 'path'; const BLOCK_MAP_FILE_SUFFIX = '.blockmap'; const argv = yargs(hideBin(process.argv)) .option('executable', { alias: 'e', type: 'string', default: 'TheiaIDE.exe', description: 'The executable for which the blockmap needs to be updated' }) .version(false) .wrap(120) .parseSync(); execute(); async function execute(): Promise { const executable = argv.executable; const executablePath = path.resolve( __dirname, '../dist/', executable ); const blockMapFile = `${executablePath}${BLOCK_MAP_FILE_SUFFIX}`; console.log(`Exe: ${executablePath}; Blockmap: ${blockMapFile}`); rmSync(blockMapFile, { force: true, }); await executeAppBuilderAsJson(['blockmap', '--input', executablePath, '--output', blockMapFile]); }; ================================================ FILE: applications/electron/scripts/update-checksum.ts ================================================ /******************************************************************************** * Copyright (C) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import * as crypto from 'crypto'; import * as fs from 'fs'; import * as jsyaml from 'js-yaml'; import * as path from 'path'; import { hideBin } from 'yargs/helpers'; import yargs from 'yargs/yargs'; const argv = yargs(hideBin(process.argv)) .option('executable', { alias: 'e', type: 'string', default: 'TheiaIDE.AppImage', description: 'The executable for which the checksum needs to be updated' }) .option('yaml', { alias: 'y', type: 'string', default: 'latest-linux.yml', description: 'The yaml file where the checksum needs to be updated' }) .option('platform', { alias: 'p', type: 'string', default: 'linux', description: 'The OS platform' }) .option('updatepaths', { alias: 'u', type: 'boolean', default: true, description: 'Whether to update the paths from absolute to relative' }) .option('fileextension', { alias: 'f', type: 'string', default: '.AppImage', description: 'Only paths/urls with this extension will be updated' }) .version(false) .wrap(120) .parseSync(); execute(); async function execute(): Promise { const executable = argv.executable; const yaml = argv.yaml; const platform = argv.platform; const updatePaths = argv.updatepaths; const fileExtension = argv.fileextension; const executablePath = path.resolve( __dirname, '../dist/', executable ); const yamlPath = path.resolve( __dirname, '../dist/', yaml ); console.log(`Exe: ${executablePath}; Yaml: ${yamlPath}; Platform: ${platform}; Update Paths: ${updatePaths}; File Extension: ${fileExtension}`); const hash = await hashFile(executablePath, 'sha512', 'base64', {}); const size = fs.statSync(executablePath).size; const yamlContents: string = fs.readFileSync(yamlPath, { encoding: 'utf8' }); console.log(`Initial Yaml Contents: ${yamlContents}`); // eslint-disable-next-line @typescript-eslint/no-explicit-any const latestYaml: any = jsyaml.safeLoad(yamlContents); if (latestYaml.path.endsWith(fileExtension)) { latestYaml.sha512 = hash; if (updatePaths) { latestYaml.path = updatedPath(latestYaml.path, latestYaml.version, platform); } } for (const file of latestYaml.files) { if (file.url.endsWith(fileExtension)) { file.sha512 = hash; file.size = size; if (updatePaths) { file.url = updatedPath(file.url, latestYaml.version, platform); } } } // line width -1 to avoid adding >- on long strings like a hash const newYamlContents = jsyaml.dump(latestYaml, { lineWidth: -1 }); console.log(`New Yaml Contents: ${newYamlContents}`); fs.writeFileSync(yamlPath, newYamlContents); } function hashFile(file: fs.PathLike, algorithm = 'sha512', encoding: BufferEncoding = 'base64', options: string | { flags?: string; encoding?: BufferEncoding; fd?: number; mode?: number; autoClose?: boolean; emitClose?: boolean; start?: number; end?: number; highWaterMark?: number; // eslint-disable-next-line @typescript-eslint/no-explicit-any }): Promise { return new Promise((resolve, reject) => { const hash = crypto.createHash(algorithm); hash.on('error', reject).setEncoding(encoding); fs.createReadStream( file, Object.assign({}, options, { highWaterMark: 1024 * 1024, }) ) .on('error', reject) .on('end', () => { hash.end(); resolve(hash.read()); }) .pipe( hash, { end: false, } ); }); } function updatedPath(toUpdate: string, version: string, platform: string): string { const extensionIndex = toUpdate.lastIndexOf('.'); return '../../' + version + '/' + platform + '/' + toUpdate.substring(0, extensionIndex) + '-' + version + toUpdate.substring(extensionIndex); } ================================================ FILE: applications/electron/test/app.spec.js ================================================ const os = require('os'); const path = require('path'); const fs = require('fs'); const { execSync } = require('child_process'); const { remote } = require('webdriverio'); const { expect } = require('chai'); const THEIA_LOAD_TIMEOUT = 15000; // 15 seconds // Set environment variable to disable splash screen (works with asar packaging) process.env.THEIA_NO_SPLASH = '1'; // Resolve the application directory from cwd so this spec can be shared across products const appDir = process.cwd(); // Directory for saving screenshots on failure or for debugging const screenshotDir = path.join(appDir, 'test-screenshots'); if (!fs.existsSync(screenshotDir)) { fs.mkdirSync(screenshotDir, { recursive: true }); } async function saveScreenshot(browser, name) { try { const filePath = path.join(screenshotDir, `${name}.png`); await browser.saveScreenshot(filePath); console.log(`Screenshot saved: ${filePath}`); } catch (err) { console.error(`Failed to save screenshot "${name}":`, err.message); } } const builderConfig = fs.readFileSync(path.join(appDir, 'electron-builder.yml'), 'utf8'); const productName = builderConfig.match(/^productName:\s*(.+)$/m)[1].trim(); const packageName = require(path.join(appDir, 'package.json')).name; function isMacArm() { if (os.platform() !== 'darwin') { return false; } try { // Check the architecture using uname -m const arch = execSync('uname -m').toString().trim(); return arch === 'arm64'; } catch (error) { // Fall back to node's arch property if uname fails return os.arch() === 'arm64'; } } function getBinaryPath() { const distFolder = path.join(appDir, 'dist'); switch (os.platform()) { case 'linux': return path.join(distFolder, 'linux-unpacked', packageName); case 'win32': return path.join(distFolder, 'win-unpacked', `${productName}.exe`); case 'darwin': const macFolder = isMacArm() ? 'mac-arm64' : 'mac'; const binaryPath = path.join( distFolder, macFolder, `${productName}.app`, 'Contents', 'MacOS', productName ); console.log(`Using binary path for Mac ${isMacArm() ? 'ARM64' : 'Intel'}: ${binaryPath}`); return binaryPath; default: return undefined; } }; // Utility for keyboard shortcuts that execute commands where // the key combination is the same on all platforms *except that* // the Command key is used instead of Control on MacOS. Note that // sometimes MacOS also uses Control. This is not handled, here function macSafeKeyCombo(keys) { if (os.platform() === 'darwin' && keys.includes('Control')) { // Puppeteer calls the Command key "Meta" return keys.map(k => k === 'Control' ? 'Meta' : k); } return keys; }; describe('Theia App', function () { // In mocha, 'this' is a common context between sibling beforeEach, afterEach, it, etc methods within the same describe. // Each describe has its own context. beforeEach(async function () { const binary = getBinaryPath(); if (!binary) { throw new Error('Tests are not supported for this platform.'); } // Start app and store connection in context (this) this.browser = await remote({ // Change to info to get detailed events of webdriverio logLevel: 'info', capabilities: { browserName: 'chrome', 'goog:chromeOptions': { // Path to built and packaged theia binary: binary, // Hand in workspace to load as runtime parameter args: [path.join(appDir, 'test', 'workspace')], }, }, }); const appShell = await this.browser.$('#theia-app-shell'); // mocha waits for returned promise to resolve // Theia is loaded once the app shell is present await appShell.waitForExist({ timeout: THEIA_LOAD_TIMEOUT, timeoutMsg: 'Theia took too long to load.', }); // If workspace trust dialog appears, trust the workspace const dialog = await this.browser.$('.dialogOverlay.workspace-trust-dialog'); const dialogAppeared = await dialog.waitForExist({ timeout: 5000 }).catch(() => false); if (dialogAppeared) { // Click the main action button to trust the workspace const trustButton = await this.browser.$('.dialogOverlay.workspace-trust-dialog .dialogControl button.theia-button.main'); const buttonClickable = await trustButton.waitForClickable({ timeout: 2000 }).catch(() => false); if (buttonClickable) { await trustButton.click(); // Wait for dialog to close await dialog.waitForExist({ timeout: 2000, reverse: true }).catch(() => { }); } } }); afterEach(async function () { // Save screenshot on test failure for debugging CI issues if (this.currentTest.state === 'failed') { const testName = this.currentTest.title.replace(/\s+/g, '-').toLowerCase(); await saveScreenshot(this.browser, `FAILED-${testName}`); } const CLOSE_TIMEOUT = 10000; // 10 seconds try { await Promise.race([ this.browser.closeWindow(), new Promise(resolve => setTimeout(resolve, CLOSE_TIMEOUT)) ]); } catch (err) { // Workaround: Puppeteer cannot properly connect to electron and throws an error. // However, the window is closed and that's all we want here. if (`${err}`.includes('Protocol error (Target.createTarget)')) { return; } // Rethrow for unexpected errors to fail test. throw err; } }); it('Correct window title', async function () { // Wait a bit to make sure workspace is set and title got updated await new Promise(r => setTimeout(r, 2000)); const windowTitle = await this.browser.getTitle(); expect(windowTitle).to.include('workspace'); }); it('Builtin extensions', async function () { // Wait a bit to make sure key handlers are registered. await new Promise(r => setTimeout(r, 5000)); // Open extensions view await this.browser.keys(macSafeKeyCombo(['Control', 'Shift', 'x'])); const builtinContainer = await this.browser.$( '#vsx-extensions-view-container--vsx-extensions\\:builtin' ); // Expand builtin extensions const builtinHeader = await builtinContainer.$('.theia-header.header'); await builtinHeader.moveTo({ xOffset: 1, yOffset: 1 }); await builtinHeader.waitForDisplayed(); await builtinHeader.waitForClickable(); await builtinHeader.click(); // Wait for expansion to finish (plugins may take time to scan, especially with asar packaging) const builtin = await this.browser.$( '#vsx-extensions\\:builtin .theia-TreeContainer' ); await builtin.waitForExist({ timeout: 10000 }); // Get names of all builtin extensions const extensions = await builtin.$$('.theia-vsx-extension .name'); const extensionNames = await Promise.all( extensions.map(e => e.getText()) ); // Exemplary check a few extensions expect(extensionNames).to.include('Debugger for Java'); expect(extensionNames).to.include('TypeScript and JavaScript Language Features (built-in)'); }); it('Search in workspace', async function () { // Wait a bit to make sure key handlers are registered await new Promise(r => setTimeout(r, 5000)); // Open search view (Ctrl+Shift+F) await this.browser.keys(macSafeKeyCombo(['Control', 'Shift', 'f'])); // Wait for search input to appear const searchInput = await this.browser.$('#search-input-field'); await searchInput.waitForExist({ timeout: 5000 }); await searchInput.waitForDisplayed(); // Search for text that exists in the test workspace README.md await searchInput.setValue('Test Workspace'); // Wait for search results to appear const searchResults = await this.browser.$('.t-siw-search-container .resultLine'); await searchResults.waitForExist({ timeout: 10000, timeoutMsg: 'Search results did not appear. Ripgrep may not be working correctly with asar packaging.' }); // Verify we got results const resultsText = await searchResults.getText(); expect(resultsText).to.include('Test Workspace'); }); it('Quick file open', async function () { // Wait a bit to make sure key handlers are registered await new Promise(r => setTimeout(r, 5000)); // Open quick file picker (Ctrl+P) await this.browser.keys(macSafeKeyCombo(['Control', 'p'])); // Wait for quick input to appear const quickInput = await this.browser.$('.quick-input-widget'); await quickInput.waitForExist({ timeout: 5000 }); await quickInput.waitForDisplayed(); // Type filename to search for const inputBox = await this.browser.$('.quick-input-box input'); await inputBox.waitForExist({ timeout: 5000 }); await inputBox.setValue('README'); // Wait for file to appear in results const fileResult = await this.browser.$('.quick-input-list-row'); await fileResult.waitForExist({ timeout: 10000, timeoutMsg: 'Quick file open results did not appear. Ripgrep may not be working correctly with asar packaging.' }); // Verify README.md appears in results const resultLabel = await this.browser.$('.quick-input-list-label'); const labelText = await resultLabel.getText(); expect(labelText.toLowerCase()).to.include('readme'); }); it('Integrated terminal', async function () { // Wait a bit to make sure key handlers are registered await new Promise(r => setTimeout(r, 5000)); // Create a new terminal (Ctrl+Shift+`) to ensure it is focused and visible, // even when the terminal manager uses tabbed layout in the bottom panel. await this.browser.keys(['Control', 'Shift', '`']); // Wait for terminal widget to appear const terminal = await this.browser.$('.xterm'); await terminal.waitForExist({ timeout: 10000, timeoutMsg: 'Terminal did not open. PTY may not be working correctly with asar packaging.' }); await terminal.waitForDisplayed(); // Verify terminal is visible const isDisplayed = await terminal.isDisplayed(); expect(isDisplayed).to.equal(true); }); }); ================================================ FILE: applications/electron/test/workspace/README.md ================================================ # Test Workspace This is the test workspace for E2E tests. ================================================ FILE: applications/electron/tsconfig.eslint.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "noEmit": true }, "include": [ "./scripts", "./test" ] } ================================================ FILE: applications/electron/tsconfig.json ================================================ { "extends": "../../configs/base.tsconfig", "include": [], "compilerOptions": { "composite": true, "esModuleInterop": true }, "references": [ { "path": "../../theia-extensions/launcher" }, { "path": "../../theia-extensions/product" }, { "path": "../../theia-extensions/updater" } ] } ================================================ FILE: applications/electron/webpack.config.js ================================================ /** * This file can be edited to customize webpack configuration. * To reset delete this file and rerun theia build again. */ // @ts-check const configs = require('./gen-webpack.config.js'); const nodeConfig = require('./gen-webpack.node.config.js'); const TerserPlugin = require('terser-webpack-plugin'); const fs = require('fs'); const path = require('path'); /** * Webpack plugin to patch the bundled ripgrep path for asar compatibility. * When packaged with asar, __dirname resolves inside app.asar but the native binaries * are extracted to app.asar.unpacked via asarUnpack. * * The native-webpack-plugin bundles ripgrep path resolution directly into main.js, * so we need to patch the bundle after emit to add asar path rewriting. */ class PatchRipgrepPlugin { apply(compiler) { compiler.hooks.afterEmit.tapAsync('PatchRipgrepPlugin', (compilation, callback) => { const mainJsPath = path.join(compiler.outputPath, 'main.js'); if (fs.existsSync(mainJsPath)) { let content = fs.readFileSync(mainJsPath, 'utf8'); // Match the ripgrep path.join(__dirname, ...) call regardless of how // the path module is referenced or how the result is exported. // Webpack output varies across modes: // Production (minified): i.join(__dirname,"./native/rg"+("win32"===...)) // Dev (harmony): (__webpack_require__(/*! path */ "path").join)(__dirname, `./native/rg${...}`) // Dev (CommonJS): path.join(__dirname, `./native/rg${...}`) // The .join call may be direct (EXPR.join(...)) or parenthesized ((EXPR.join)(...)). // Both string concat (prod) and template literal (dev) arg forms are matched. const rgSuffix = `("win32"===process.platform?".exe":"")`; const prodArgs = /["']\.\/native\/rg["']\s*\+\s*\(["']win32["']\s*===\s*process\.platform\s*\?\s*["']\.exe["']\s*:\s*["']["']\s*\)/; const devArgs = /`\.\/native\/rg\$\{process\.platform\s*===\s*['"]win32['"]\s*\?\s*['"]\.exe['"]\s*:\s*['"]['"]}\s*`/; // Match both EXPR.join(__dirname, ARGS) and (EXPR.join)(__dirname, ARGS) // Use [^=,;\n] (not \s) to allow spaces in webpack comments like /*! path */ const joinCall = (argsPattern) => new RegExp(`\\(?[^=,;\\n]+?\\.join\\)?\\(\\s*__dirname\\s*,\\s*${argsPattern.source}\\s*\\)`, 'g'); const prodPattern = joinCall(prodArgs); const devPattern = joinCall(devArgs); let patched = false; const patchFn = (match) => { patched = true; return `(()=>{const _p=require("path"),_r=_p.join(__dirname,"./native/rg"+${rgSuffix});return _r.includes(".asar"+_p.sep)?_r.replace(".asar"+_p.sep,".asar.unpacked"+_p.sep):_r})()`; }; let newContent = content.replace(prodPattern, patchFn); if (!patched) { newContent = content.replace(devPattern, patchFn); } if (patched) { fs.writeFileSync(mainJsPath, newContent); console.log('Patched main.js ripgrep path for asar compatibility'); } else { const idx = content.indexOf('rgPath'); if (idx !== -1) { console.error('Could not patch ripgrep path. Context around rgPath:'); console.error(content.substring(Math.max(0, idx - 200), idx + 300)); } throw new Error('Could not find ripgrep pattern to patch in main.js. The pattern may have changed in @theia/native-webpack-plugin.'); } } callback(); }); } } /** * Expose bundled modules on window.theia.moduleName namespace, e.g. * window['theia']['@theia/core/lib/common/uri']. * Such syntax can be used by external code, for instance, for testing. configs[0].module.rules.push({ test: /\.js$/, loader: require.resolve('@theia/application-manager/lib/expose-loader') }); */ /** * Do no run TerserPlugin with parallel: true * Each spawned node may take the full memory configured via NODE_OPTIONS / --max_old_space_size * In total this may lead to OOM issues */ if (nodeConfig.config.optimization) { nodeConfig.config.optimization.minimizer = [ new TerserPlugin({ parallel: false, exclude: /^(lib|builtins)\//, terserOptions: { keep_classnames: /AbortSignal/ } }) ]; } for (const config of configs) { config.optimization = { minimizer: [ new TerserPlugin({ parallel: false }) ] }; } // Add the ripgrep patch plugin to the node config nodeConfig.config.plugins = nodeConfig.config.plugins || []; nodeConfig.config.plugins.push(new PatchRipgrepPlugin()); module.exports = [ ...configs, nodeConfig.config ]; ================================================ FILE: applications/electron-next/.eslintrc.js ================================================ /** @type {import('eslint').Linter.Config} */ module.exports = { extends: [ '../../configs/build.eslintrc.json' ], parserOptions: { tsconfigRootDir: __dirname, project: 'tsconfig.eslint.json' } }; ================================================ FILE: applications/electron-next/electron-builder.yml ================================================ appId: eclipse.theia.next productName: TheiaIDENext copyright: Copyright © 2020-2025 Eclipse Foundation, Inc electronDist: ../../node_modules/electron/dist electronVersion: 39.8.7 asar: true asarUnpack: - "**/lib/backend/native/**" - "**/lib/backend/shell-integrations/**" - "**/lib/build/Release/**" - "**/lib/prebuilds/**" nodeGypRebuild: false npmRebuild: false directories: buildResources: resources # node_modules and package.json are copied automatically # Exclude node_modules manually because electron is copied by electron-builder and we are using a bundled backend files: - src-gen - lib - resources/icons/WindowIcon/512-512.png - resources/TheiaIDENextSplash.svg - scripts - "!**node_modules/**" extraResources: - from: ../../plugins to: app/plugins linux: icon: resources/icons/LinuxLauncherIcons category: Development mimeTypes: - inode/directory vendor: Eclipse Foundation, Inc target: - deb - AppImage deb: artifactName: ${productName}.${ext} appImage: artifactName: ${productName}.${ext} afterPack: ./scripts/after-pack.js ================================================ FILE: applications/electron-next/package.json ================================================ { "private": true, "name": "theia-ide-next-electron-app", "description": "Eclipse Theia IDE Next product", "productName": "Theia IDE Next", "version": "1.71.100", "main": "scripts/theia-electron-main.js", "license": "MIT", "author": "Eclipse Theia ", "homepage": "https://github.com/eclipse-theia/theia-ide#readme", "bugs": { "url": "https://github.com/eclipse-theia/theia/issues" }, "repository": { "type": "git", "url": "git+https://github.com/eclipse-theia/theia-ide.git" }, "engines": { "yarn": ">=1.7.0 <2", "node": ">=22" }, "theia": { "target": "electron", "frontend": { "config": { "applicationName": "Theia IDE Next", "brandingVariant": "next", "availableUpdateChannels": [ "next" ], "reloadOnReconnect": true, "preferences": { "toolbar.showToolbar": true, "updates.channel": "next", "ai-features.chat.tokenUsageIndicator.enabled": true }, "electron": { "uriScheme": "theia-next", "showWindowEarly": false, "splashScreenOptions": { "content": "resources/TheiaIDENextSplash.svg", "height": 276, "width": 446 } } } }, "backend": { "config": { "frontendConnectionTimeout": -1, "startupTimeout": -1, "resolveSystemPlugins": false, "configurationFolder": ".theia-ide-next" } }, "generator": { "config": { "preloadTemplate": "./resources/preload.html" } } }, "dependencies": { "@theia/ai-anthropic": "1.72.0-next.20", "@theia/ai-chat": "1.72.0-next.20", "@theia/ai-chat-ui": "1.72.0-next.20", "@theia/ai-claude-code": "1.72.0-next.20", "@theia/ai-code-completion": "1.72.0-next.20", "@theia/ai-codex": "1.72.0-next.20", "@theia/ai-copilot": "1.72.0-next.20", "@theia/ai-core": "1.72.0-next.20", "@theia/ai-core-ui": "1.72.0-next.20", "@theia/ai-editor": "1.72.0-next.20", "@theia/ai-google": "1.72.0-next.20", "@theia/ai-history": "1.72.0-next.20", "@theia/ai-huggingface": "1.72.0-next.20", "@theia/ai-ide": "1.72.0-next.20", "@theia/ai-llamafile": "1.72.0-next.20", "@theia/ai-mcp": "1.72.0-next.20", "@theia/ai-mcp-server": "1.72.0-next.20", "@theia/ai-mcp-ui": "1.72.0-next.20", "@theia/ai-ollama": "1.72.0-next.20", "@theia/ai-openai": "1.72.0-next.20", "@theia/ai-scanoss": "1.72.0-next.20", "@theia/ai-terminal": "1.72.0-next.20", "@theia/ai-vercel-ai": "1.72.0-next.20", "@theia/bulk-edit": "1.72.0-next.20", "@theia/callhierarchy": "1.72.0-next.20", "@theia/collaboration": "1.72.0-next.20", "@theia/console": "1.72.0-next.20", "@theia/core": "1.72.0-next.20", "@theia/debug": "1.72.0-next.20", "@theia/dev-container": "1.72.0-next.20", "@theia/editor": "1.72.0-next.20", "@theia/editor-preview": "1.72.0-next.20", "@theia/electron": "1.72.0-next.20", "@theia/external-terminal": "1.72.0-next.20", "@theia/file-search": "1.72.0-next.20", "@theia/filesystem": "1.72.0-next.20", "@theia/getting-started": "1.72.0-next.20", "@theia/keymaps": "1.72.0-next.20", "@theia/markers": "1.72.0-next.20", "@theia/memory-inspector": "1.72.0-next.20", "@theia/messages": "1.72.0-next.20", "@theia/metrics": "1.72.0-next.20", "@theia/mini-browser": "1.72.0-next.20", "@theia/monaco": "1.72.0-next.20", "@theia/navigator": "1.72.0-next.20", "@theia/notebook": "1.72.0-next.20", "@theia/outline-view": "1.72.0-next.20", "@theia/output": "1.72.0-next.20", "@theia/plugin-dev": "1.72.0-next.20", "@theia/plugin-ext": "1.72.0-next.20", "@theia/plugin-ext-vscode": "1.72.0-next.20", "@theia/preferences": "1.72.0-next.20", "@theia/preview": "1.72.0-next.20", "@theia/process": "1.72.0-next.20", "@theia/property-view": "1.72.0-next.20", "@theia/remote": "1.72.0-next.20", "@theia/remote-wsl": "1.72.0-next.20", "@theia/scanoss": "1.72.0-next.20", "@theia/scm": "1.72.0-next.20", "@theia/search-in-workspace": "1.72.0-next.20", "@theia/secondary-window": "1.72.0-next.20", "@theia/task": "1.72.0-next.20", "@theia/terminal": "1.72.0-next.20", "@theia/terminal-manager": "1.72.0-next.20", "@theia/test": "1.72.0-next.20", "@theia/timeline": "1.72.0-next.20", "@theia/toolbar": "1.72.0-next.20", "@theia/typehierarchy": "1.72.0-next.20", "@theia/userstorage": "1.72.0-next.20", "@theia/variable-resolver": "1.72.0-next.20", "@theia/vsx-registry": "1.72.0-next.20", "@theia/workspace": "1.72.0-next.20", "fs-extra": "^9.1.0", "theia-ide-launcher-ext": "1.71.100", "theia-ide-product-ext": "1.71.100", "theia-ide-updater-ext": "1.71.100" }, "devDependencies": { "@theia/cli": "1.72.0-next.20", "@theia/bundle-plugin": "1.72.0-next.20", "@types/js-yaml": "^3.12.10", "@types/yargs": "17.0.7", "@wdio/cli": "^6.12.1", "@wdio/local-runner": "^6.12.1", "@wdio/mocha-framework": "^6.11.0", "@wdio/spec-reporter": "^6.11.0", "app-builder-lib": "26.0.12", "chai": "^4.5.0", "concurrently": "^3.6.1", "electron": "39.8.7", "electron-builder": "26.0.12", "electron-chromedriver": "^28.3.3", "electron-mocha": "^12.3.1", "electron-osx-sign": "^0.6.0", "js-yaml": "^3.14.2", "mocha": "^8.4.0", "rimraf": "^2.7.1", "ts-node": "^10.9.2", "wdio-chromedriver-service": "^6.0.4", "webdriverio": "^6.12.1", "yargs": "17.2.1" }, "scripts": { "clean": "theia clean && rimraf node_modules", "clean:dist": "rimraf dist", "build": "yarn -s rebuild && theia build --app-target=\"electron\" --mode development", "build:prod": "yarn -s rebuild && theia build --app-target=\"electron\"", "rebuild": "theia rebuild:electron --cacheRoot ../..", "watch": "concurrently -n compile,build \"theiaext watch --preserveWatchOutput\" \"theia build --watch --mode development\"", "start": "electron scripts/theia-electron-main.js --plugins=local-dir:../../plugins", "start:debug": "yarn start --log-level=debug", "package": "yarn clean:dist && yarn rebuild && electron-builder -c.mac.identity=null --publish never", "package:prod": "yarn deploy", "deploy": "yarn clean:dist && yarn rebuild && electron-builder -c.mac.identity=null --publish always", "package:preview": "yarn clean:dist && yarn rebuild && electron-builder -c.mac.identity=null --dir", "update:theia": "ts-node ../../scripts/update-theia-version.ts", "update:next": "ts-node ../../scripts/update-theia-version.ts next", "test": "mocha --timeout 60000 \"../electron/test/*.spec.js\"", "lint": "eslint --ext js,jsx,ts,tsx scripts", "lint:fix": "eslint --ext js,jsx,ts,tsx scripts --fix" } } ================================================ FILE: applications/electron-next/resources/LICENSE ================================================ MIT License Copyright (c) 2026 Eclipse Theia IDE Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: applications/electron-next/resources/preload.html ================================================
================================================ FILE: applications/electron-next/scripts/after-pack.js ================================================ #!/usr/bin/env node const fs = require('fs'); const path = require('path'); // Signing and notarizing are not needed for the Next product // (it is not built on Jenkins as a full release). // Only the Linux sandbox fix is required for AppImage builds. // taken and modified from: https://github.com/gergof/electron-builder-sandbox-fix/blob/a2251d7d8f22be807d2142da0cf768c78d4cfb0a/lib/index.js exports.default = async function (context) { if (context.electronPlatformName !== 'linux') { return; } const executable = path.join( context.appOutDir, context.packager.executableName ); const loaderScript = `#!/usr/bin/env bash set -u SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )" exec "$SCRIPT_DIR/${context.packager.executableName}.bin" "--no-sandbox" "$@" `; try { await fs.promises.rename(executable, executable + '.bin'); await fs.promises.writeFile(executable, loaderScript); await fs.promises.chmod(executable, 0o755); } catch (e) { throw new Error('Failed to create loader for sandbox fix:\n' + e); } }; ================================================ FILE: applications/electron-next/scripts/appimage-helpers.js ================================================ const fs = require('fs'); const path = require('path'); /** * Reads the plugin copy metadata file and returns its content. * @param metadataPath - Path to the metadata file * @returns The metadata object or undefined if not found */ function readPluginCopyMetadata(metadataPath) { if (!fs.existsSync(metadataPath)) { return undefined; } try { return JSON.parse(fs.readFileSync(metadataPath, 'utf8')); } catch (err) { console.warn('Could not read built-in plugin copy metadata file:', err.message); return undefined; } } /** * Writes the plugin copy metadata file with version and timestamp. * @param metadataPath - Path to the metadata file * @param version - Current version */ function writePluginCopyMetadata(metadataPath, version) { const metadata = { version: version, copiedAt: new Date().toISOString() }; fs.writeFileSync(metadataPath, JSON.stringify(metadata, undefined, 2)); } /** * Copies bundled plugins from AppImage to user directory if needed. * @param bundledPluginsDir - Path to bundled plugins in AppImage * @param userPluginsDir - Path to user built-in plugins directory * @param currentVersion - Current Theia IDE version * @returns true if the builtins were copied to the user dir, false if there was an error */ function copyBundledPlugins(bundledPluginsDir, userPluginsDir, currentVersion) { const metadataFile = path.join(userPluginsDir, '.builtInPlugins-metadata'); // Ensure the user plugins directory exists if (!fs.existsSync(userPluginsDir)) { fs.mkdirSync(userPluginsDir, { recursive: true }); } // Check if built-in plugins need to be copied const metadata = readPluginCopyMetadata(metadataFile); let shouldCopy = false; if (!metadata) { shouldCopy = true; } else if (metadata.version !== currentVersion) { console.log(`Theia IDE updated from ${metadata.version} to ${currentVersion}. Updating built-in plugins...`); shouldCopy = true; } if (!shouldCopy) { console.log('Built-in plugins were already copied.'); return true; } console.log(`Copying bundled plugins from AppImage to ${userPluginsDir}...`); try { // Clean existing plugins directory to remove old/obsolete plugins fs.rmSync(userPluginsDir, { recursive: true, force: true }); fs.mkdirSync(userPluginsDir, { recursive: true }); const pluginEntries = fs.readdirSync(bundledPluginsDir, { withFileTypes: true }); for (const entry of pluginEntries) { const srcPath = path.join(bundledPluginsDir, entry.name); const destPath = path.join(userPluginsDir, entry.name); fs.cpSync(srcPath, destPath, { recursive: true }); } writePluginCopyMetadata(metadataFile, currentVersion); console.log(`Bundled plugins copied successfully to ${userPluginsDir}.`); } catch (err) { console.error('Failed to copy bundled plugins:', err.message); return false; } return true; } module.exports = { copyBundledPlugins, }; ================================================ FILE: applications/electron-next/scripts/theia-electron-main.js ================================================ const path = require('path'); const fs = require('fs'); const os = require('os'); const { copyBundledPlugins } = require('./appimage-helpers'); // Update to override the supported VS Code API version. // process.env.VSCODE_API_VERSION = '1.50.0' // Detect if running as AppImage const isAppImage = !!process.env.APPIMAGE; // When packaged with asar, __dirname is inside app.asar (e.g., .../app.asar/scripts) // but plugins are in extraResources at .../app/plugins (outside the asar) const isInsideAsar = __dirname.includes('.asar'); const bundledPluginsDir = isInsideAsar ? path.join(process.resourcesPath, 'app', 'plugins') : path.resolve(__dirname, '../', 'plugins'); if (isAppImage) { // When running as AppImage, use a user-writable directory for the built-in plugins // The AppImage mount point (/tmp/.mount_*) is read-only const configDir = process.env.THEIA_CONFIG_DIR || path.join(os.homedir(), '.theia-ide-next'); const userPluginsDir = path.join(configDir, 'builtInPlugins'); const packageJsonPath = path.resolve(__dirname, '../', 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const currentVersion = packageJson.version; // Copy bundled plugins to user directory if needed (first run or version update) const useUserDir = copyBundledPlugins(bundledPluginsDir, userPluginsDir, currentVersion); // If copying fails, fall back to the read-only bundled directory (will be improved in follow up of GH-630) process.env.THEIA_DEFAULT_PLUGINS = `local-dir:${useUserDir ? userPluginsDir : bundledPluginsDir}`; } else { // Use a set of builtin plugins in our application. process.env.THEIA_DEFAULT_PLUGINS = `local-dir:${bundledPluginsDir}`; } // Handover to the auto-generated electron application handler. require('../lib/backend/electron-main.js'); ================================================ FILE: applications/electron-next/test/workspace/README.md ================================================ # Test Workspace This is the test workspace for E2E tests. ================================================ FILE: applications/electron-next/tsconfig.eslint.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "noEmit": true }, "include": [ "./scripts" ] } ================================================ FILE: applications/electron-next/tsconfig.json ================================================ { "extends": "../../configs/base.tsconfig", "include": [], "compilerOptions": { "composite": true, "esModuleInterop": true }, "references": [ { "path": "../../theia-extensions/product" }, { "path": "../../theia-extensions/updater" } ] } ================================================ FILE: applications/electron-next/webpack.config.js ================================================ /** * This file can be edited to customize webpack configuration. * To reset delete this file and rerun theia build again. */ // @ts-check const configs = require('./gen-webpack.config.js'); const nodeConfig = require('./gen-webpack.node.config.js'); const TerserPlugin = require('terser-webpack-plugin'); const fs = require('fs'); const path = require('path'); /** * Webpack plugin to patch the bundled ripgrep path for asar compatibility. * When packaged with asar, __dirname resolves inside app.asar but the native binaries * are extracted to app.asar.unpacked via asarUnpack. * * The native-webpack-plugin bundles ripgrep path resolution directly into main.js, * so we need to patch the bundle after emit to add asar path rewriting. */ class PatchRipgrepPlugin { apply(compiler) { compiler.hooks.afterEmit.tapAsync('PatchRipgrepPlugin', (compilation, callback) => { const mainJsPath = path.join(compiler.outputPath, 'main.js'); if (fs.existsSync(mainJsPath)) { let content = fs.readFileSync(mainJsPath, 'utf8'); // Match the ripgrep path.join(__dirname, ...) call regardless of how // the path module is referenced or how the result is exported. // Webpack output varies across modes: // Production (minified): i.join(__dirname,"./native/rg"+("win32"===...)) // Dev (harmony): (__webpack_require__(/*! path */ "path").join)(__dirname, `./native/rg${...}`) // Dev (CommonJS): path.join(__dirname, `./native/rg${...}`) // The .join call may be direct (EXPR.join(...)) or parenthesized ((EXPR.join)(...)). // Both string concat (prod) and template literal (dev) arg forms are matched. const rgSuffix = `("win32"===process.platform?".exe":"")`; const prodArgs = /["']\.\/native\/rg["']\s*\+\s*\(["']win32["']\s*===\s*process\.platform\s*\?\s*["']\.exe["']\s*:\s*["']["']\s*\)/; const devArgs = /`\.\/native\/rg\$\{process\.platform\s*===\s*['"]win32['"]\s*\?\s*['"]\.exe['"]\s*:\s*['"]['"]}\s*`/; // Match both EXPR.join(__dirname, ARGS) and (EXPR.join)(__dirname, ARGS) // Use [^=,;\n] (not \s) to allow spaces in webpack comments like /*! path */ const joinCall = (argsPattern) => new RegExp(`\\(?[^=,;\\n]+?\\.join\\)?\\(\\s*__dirname\\s*,\\s*${argsPattern.source}\\s*\\)`, 'g'); const prodPattern = joinCall(prodArgs); const devPattern = joinCall(devArgs); let patched = false; const patchFn = (match) => { patched = true; return `(()=>{const _p=require("path"),_r=_p.join(__dirname,"./native/rg"+${rgSuffix});return _r.includes(".asar"+_p.sep)?_r.replace(".asar"+_p.sep,".asar.unpacked"+_p.sep):_r})()`; }; let newContent = content.replace(prodPattern, patchFn); if (!patched) { newContent = content.replace(devPattern, patchFn); } if (patched) { fs.writeFileSync(mainJsPath, newContent); console.log('Patched main.js ripgrep path for asar compatibility'); } else { const idx = content.indexOf('rgPath'); if (idx !== -1) { console.error('Could not patch ripgrep path. Context around rgPath:'); console.error(content.substring(Math.max(0, idx - 200), idx + 300)); } throw new Error('Could not find ripgrep pattern to patch in main.js. The pattern may have changed in @theia/native-webpack-plugin.'); } } callback(); }); } } /** * Expose bundled modules on window.theia.moduleName namespace, e.g. * window['theia']['@theia/core/lib/common/uri']. * Such syntax can be used by external code, for instance, for testing. configs[0].module.rules.push({ test: /\.js$/, loader: require.resolve('@theia/application-manager/lib/expose-loader') }); */ /** * Do no run TerserPlugin with parallel: true * Each spawned node may take the full memory configured via NODE_OPTIONS / --max_old_space_size * In total this may lead to OOM issues */ if (nodeConfig.config.optimization) { nodeConfig.config.optimization.minimizer = [ new TerserPlugin({ parallel: false, exclude: /^(lib|builtins)\//, terserOptions: { keep_classnames: /AbortSignal/ } }) ]; } for (const config of configs) { config.optimization = { minimizer: [ new TerserPlugin({ parallel: false }) ] }; } // Add the ripgrep patch plugin to the node config nodeConfig.config.plugins = nodeConfig.config.plugins || []; nodeConfig.config.plugins.push(new PatchRipgrepPlugin()); module.exports = [ ...configs, nodeConfig.config ]; ================================================ FILE: browser.Dockerfile ================================================ # Builder stage FROM node:24-bookworm AS build-stage # install required tools to build the application RUN apt-get update && apt-get install -y libxkbfile-dev libsecret-1-dev WORKDIR /home/theia # Copy repository files COPY . . # Remove unnecesarry files for the browser application # Download plugins and build application production mode # Use yarn autoclean to remove unnecessary files from package dependencies RUN yarn config set network-timeout 600000 -g && \ yarn --pure-lockfile && \ yarn build:extensions && \ yarn download:plugins && \ yarn browser build && \ yarn && \ yarn autoclean --init && \ echo *.ts >> .yarnclean && \ echo *.ts.map >> .yarnclean && \ echo *.spec.* >> .yarnclean && \ yarn autoclean --force && \ yarn cache clean && \ rm -rf .git applications/electron theia-extensions/launcher theia-extensions/updater node_modules # Production stage uses a small base image FROM node:24-bookworm-slim AS production-stage # Create theia user and directories # Application will be copied to /home/theia # Default workspace is located at /home/project RUN adduser --system --group --home /home/theia theia RUN chmod g+rw /home && \ mkdir -p /home/project && \ chown -R theia:theia /home/theia && \ chown -R theia:theia /home/project; # Install required tools for application: Temurin JDK, JDK, SSH, Bash, Maven # Node is already available in base image RUN apt-get update && apt-get install -y wget apt-transport-https && \ apt-get update && apt-get install -y git openssh-client openssh-server bash libsecret-1-0 openjdk-17-jdk maven && \ apt-get purge -y wget && \ apt-get clean ENV HOME=/home/theia WORKDIR /home/theia # Copy application from builder-stage COPY --from=build-stage --chown=theia:theia /home/theia /home/theia EXPOSE 3000 # Specify default shell for Theia and the Built-In plugins directory ENV SHELL=/bin/bash \ THEIA_DEFAULT_PLUGINS=local-dir:/home/theia/plugins # Use installed git instead of dugite ENV USE_LOCAL_GIT=true # Switch to Theia user USER theia WORKDIR /home/theia/applications/browser # Launch the backend application via node ENTRYPOINT [ "node", "/home/theia/applications/browser/lib/backend/main.js" ] # Arguments passed to the application CMD [ "/home/project", "--hostname=0.0.0.0" ] ================================================ FILE: cleanup/Jenkinsfile ================================================ pipeline { agent { label 'windows' } triggers { cron('@weekly') } options { timeout(time: 1, unit: 'HOURS') disableConcurrentBuilds() } stages { stage('Cleanup Windows temp directory') { steps { script { listTemp('before cleanup', '%temp%') cleanFiles('tmp/plugin-download', '%temp%', 'theia-plugin-download**') cleanDirs('tmp/yarn', '%temp%', 'yarn--**') cleanDirs('tmp/lighthouse', '%temp%', 'lighthouse.*') listTemp('after cleanup', '%temp%') cleanDir('appdata/local/electron', '\"%LocalAppData%\"\\electron\\Cache') cleanYarnCache('appdata/local/yarn') } } } } } Object listTemp(String label, String temp) { echo "in listTemp(): ${label}" bat "DIR ${temp}" return } Object cleanFiles(String label, String parent, String pattern) { echo "in cleanFile() - clean: ${label} files" echo "parent: ${parent}, pattern: ${pattern}" // use "returnStatus" option to avoid an exception being thrown if no // matching files are found, failing the pipeline s = bat( script: "FORFILES /p ${parent} /m ${pattern} /C \"cmd /c Del /q @file\"", returnStatus: true ) if (s != 0) { echo "No ${pattern} file found... Good I guess" } } Object cleanDirs(String label, String parent, String pattern) { echo "Before ${label} Cleanup:" bat "FOR /D /R ${parent} %%i in (${pattern}) do echo \"%%i\"" bat "FOR /D /R ${parent} %%i in (${pattern}) do @rmdir /s /q \"%%i\"" echo "After ${label} Cleanup:" bat "FOR /D /R ${parent} %%i in (${pattern}) do echo \"%%i\"" return } Object cleanDir(String label, String parent) { echo "Before ${label} Cleanup:" bat "FOR /D /R ${parent} %%i in (*) do echo \"%%i\"" bat "if exist ${parent} @rmdir /s /q ${parent}" echo "After ${label} Cleanup:" bat "FOR /D /R ${parent} %%i in (*) do echo \"%%i\"" return } Object cleanYarnCache(String label) { echo "Cleaning-up: ${label}" sh 'yarn cache clean --all' return } ================================================ FILE: configs/base.eslintrc.json ================================================ { "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", "ecmaVersion": 6, "ecmaFeatures": { "jsx": true } }, "plugins": [ "@typescript-eslint", "@typescript-eslint/tslint", "import", "no-null" ], "env": { "browser": true, "mocha": true, "node": true }, "ignorePatterns": [ "node_modules", "lib" ] } ================================================ FILE: configs/base.tsconfig.json ================================================ { "compilerOptions": { "skipLibCheck": true, "declaration": true, "declarationMap": true, "noImplicitAny": true, "noEmitOnError": false, "noImplicitThis": true, "noUnusedLocals": true, "strictNullChecks": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "downlevelIteration": true, "resolveJsonModule": true, "module": "commonjs", "moduleResolution": "node", "target": "ES2017", "jsx": "react", "lib": [ "ES2017", "ES2020.Promise", "dom" ], "sourceMap": true, "composite": true } } ================================================ FILE: configs/build.eslintrc.json ================================================ { "extends": [ "./base.eslintrc.json", "./errors.eslintrc.json" ] } ================================================ FILE: configs/errors.eslintrc.json ================================================ { "$schema": "https://json.schemastore.org/eslintrc", "rules": { "@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/indent": "off", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/quotes": [ "error", "single", { "avoidEscape": true } ], "@typescript-eslint/semi": [ "error", "always" ], "@typescript-eslint/type-annotation-spacing": "error", "arrow-body-style": [ "error", "as-needed" ], "arrow-parens": [ "error", "as-needed" ], "camelcase": "off", "comma-dangle": "off", "curly": "error", "eol-last": "error", "eqeqeq": [ "error", "smart" ], "guard-for-in": "error", "id-blacklist": "off", "id-match": "off", "max-len": [ "error", { "code": 180 } ], "no-magic-numbers": "off", "no-multiple-empty-lines": [ "error", { "max": 1 } ], "no-new-wrappers": "error", "no-null/no-null": "error", "no-shadow": "off", "@typescript-eslint/no-shadow": [ "error", { "hoist": "all" } ], "no-tabs": "error", "no-throw-literal": "error", "no-trailing-spaces": "error", "no-underscore-dangle": "off", "no-unused-expressions": "error", "no-var": "error", "no-void": "error", "one-var": [ "error", "never" ], "prefer-const": [ "error", { "destructuring": "all" } ], "radix": "off", "space-before-function-paren": [ "error", { "anonymous": "always", "named": "never", "asyncArrow": "always" } ], "spaced-comment": [ "error", "always", { "exceptions": [ "*", "+", "-", "/" ] } ], "@typescript-eslint/tslint/config": [ "error", { "rules": { "file-header": [ true, "SPDX-License-Identifier: MIT" ], "jsdoc-format": [ true, "check-multiline-start" ], "one-line": [ true, "check-open-brace", "check-catch", "check-else", "check-whitespace" ], "typedef": [ true, "call-signature", "property-declaration" ], "whitespace": [ true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type" ] } } ], "import/no-extraneous-dependencies": "error" }, "overrides": [ { "files": [ "dev-packages", "*.{spec,espec,slow-spec}.{js,ts}" ], "rules": { "import/no-extraneous-dependencies": "off" } } ] } ================================================ FILE: configs/license-check-config.json ================================================ { "project": "ecd.theia", "inputFile": "yarn.lock", "batch": 50, "timeout": 240, "summary": "license-check-summary.txt" } ================================================ FILE: configs/tsconfig.eslint.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "noEmit": true }, "include": [ "../scripts" ] } ================================================ FILE: configs/warnings.eslintrc.json ================================================ { "plugins": [ "deprecation" ], "rules": { "@typescript-eslint/await-thenable": "warn", "no-return-await": "warn", "deprecation/deprecation": "warn" } } ================================================ FILE: configs/xss.eslintrc.json ================================================ { "extends": ["plugin:no-unsanitized/DOM"], "plugins": ["no-unsanitized", "react"], "parserOptions": { "ecmaFeatures": { "jsx": true } }, "rules": { "no-unsanitized/method": [ "warn", { "escape": { "methods": ["DOMPurify.sanitize"] } } ], "no-unsanitized/property": [ "warn", { "escape": { "methods": ["DOMPurify.sanitize"] } } ], "no-eval": "warn", "no-implied-eval": "warn", "react/no-danger-with-children": "warn", "react/no-danger": "warn" } } ================================================ FILE: docs/developing-with-local-theia.md ================================================ # Developing with a Local Theia Framework This guide explains how to build and test the Theia IDE against a local development version of the [Theia framework](https://github.com/eclipse-theia/theia). This is useful when you need to: - Test Theia IDE changes against unreleased Theia features - Debug issues that span both the framework and the Theia IDE - Develop new Theia features and immediately test them in the Theia IDE ## Prerequisites - Node.js and npm installed (see [Theia prerequisites](https://github.com/eclipse-theia/theia/blob/master/doc/Developing.md#prerequisites)) - A local clone of the [Theia repository](https://github.com/eclipse-theia/theia) The recommended setup is to have both repositories cloned as siblings: ```text parent-directory/ theia/ # Theia framework theia-ide/ # This repository ``` This matches the script's default `--theia-path` of `../theia`. You can clone Theia anywhere and specify the path with `--theia-path`. ## Important Note This script does not update the IDE version or Theia package versions in `package.json` files. It uses the current state of both repositories and relies on yarn linking to override the dependencies. If needed, you can run versioning commands (e.g., `yarn update:theia `) separately before building. ## Quick Start ```sh # Clone Theia as a sibling (if not already done) git clone https://github.com/eclipse-theia/theia.git ../theia # Build everything node scripts/build-with-local-theia.js ``` ## What the Script Does 1. Build the local Theia framework (`npm ci` + `npm run compile`) 2. Create yarn links for all `@theia/*` packages 3. Link those packages into the Theia IDE 4. Build the Theia IDE extensions and electron-next application 5. Download required plugins ## Usage ### Full Build ```sh node scripts/build-with-local-theia.js ``` ### Using a Different Theia Location ```sh node scripts/build-with-local-theia.js --theia-path /path/to/theia ``` ### Incremental Development After the initial build, you can iterate faster: ```sh # Rebuild only Theia IDE (Theia unchanged) node scripts/build-with-local-theia.js --skip-theia-build # Rebuild only Theia packages, then rebuild Theia IDE cd ../theia && npm run compile cd ../theia-ide && yarn build:applications:next:dev ``` ### Build and Package To create a distributable application: ```sh node scripts/build-with-local-theia.js --package ``` The packaged application will be in `applications/electron-next/dist/`. ### Set Up Links Only If you want to manage builds manually: ```sh node scripts/build-with-local-theia.js --skip-theia-build --skip-ide-build ``` ### Skip Plugin Download If you already have plugins or want to skip downloading them: ```sh node scripts/build-with-local-theia.js --skip-plugins ``` ### Restore Normal Dependencies When you're done testing with the local Theia: ```sh node scripts/build-with-local-theia.js --unlink ``` This removes all yarn links and reinstalls packages from npm. ### Dry Run Preview what commands will be executed: ```sh node scripts/build-with-local-theia.js --dry-run ``` ## Running the Theia IDE (Next) After building: ```sh yarn --cwd applications/electron-next start ``` If you packaged the application with `--package`, you can also run the packaged version directly from `applications/electron-next/dist/`. ## Development Workflow A typical development workflow looks like: 1. **Initial setup**: Clone Theia and run the full build ```sh git clone https://github.com/eclipse-theia/theia.git ../theia node scripts/build-with-local-theia.js ``` 2. **Make changes in Theia**: Edit files in `../theia` 3. **Rebuild Theia**: ```sh cd ../theia && npm run compile ``` 4. **Rebuild and run Theia IDE**: ```sh cd ../theia-ide yarn build:applications:next:dev yarn --cwd applications/electron-next start ``` 5. **When done**: Restore npm dependencies ```sh node scripts/build-with-local-theia.js --unlink ``` ## Command Reference | Option | Description | |-----------------------|------------------------------------------------------| | `--theia-path ` | Path to local Theia repository (default: `../theia`) | | `--skip-theia-build` | Skip building Theia packages (use if already built) | | `--skip-ide-build` | Skip building Theia IDE (use for linking only) | | `--skip-plugins` | Skip downloading plugins | | `--package` | Package the electron-next application after building | | `--unlink` | Remove links and restore npm dependencies | | `--dry-run` | Print commands without executing them | | `--help` | Show help message | ## Troubleshooting ### "Theia directory not found" Make sure you have cloned the Theia repository: ```sh git clone https://github.com/eclipse-theia/theia.git ../theia ``` Or specify the correct path: ```sh node scripts/build-with-local-theia.js --theia-path /correct/path/to/theia ``` ### "Package not found in local Theia" Some `@theia/*` packages used by the Theia IDE may not exist in your Theia checkout. This can happen if: - You're on an older Theia branch that doesn't have newer packages - The package is from a different source The script will warn about missing packages but continue with available ones. ### Build Errors After Switching Branches If you switch branches in either repository, clean and rebuild: ```sh # In theia-ide git clean -xfd node scripts/build-with-local-theia.js # Or if only Theia packages changed cd ../theia && git clean -xfd && npm ci && npm run compile cd ../theia-ide && yarn build:applications:next:dev ``` ### Restoring Clean State If things get into a bad state: ```sh # Unlink and restore npm packages node scripts/build-with-local-theia.js --unlink # Full clean rebuild git clean -xfd yarn && yarn build:dev && yarn download:plugins ``` ================================================ FILE: lerna.json ================================================ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "version": "1.71.100", "command": { "run": { "stream": true } } } ================================================ FILE: next/Jenkinsfile ================================================ /** * This Jenkinsfile builds Theia Next across the major OS platforms */ import groovy.json.JsonSlurper distFolder = "applications/electron/dist" pipeline { agent none triggers { cron('@daily') } options { timeout(time: 5, unit: 'HOURS') disableConcurrentBuilds() } environment { NODE_OPTIONS = '--max_old_space_size=4096' } stages { stage('Build') { parallel { stage('Test Linux Theia@Next') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "8Gi" cpu: "2" requests: memory: "8Gi" cpu: "2" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} """ } } steps { container('theia-dev') { withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { script { buildNext(false, 1200) } } } } post { failure { error("Linux installer creation failed, aborting...") } } } stage('Test Mac Theia@Next') { agent { label 'macos' } steps { script { buildNext(false, 60) } } post { failure { error("Mac installer creation failed, aborting...") } } } stage('Test Windows Theia@Next') { agent { label 'windows' } steps { script { sh "npm config set msvs_version 2017" sh "npx node-gyp install 14.20.0" buildNext(true, 60) } } post { failure { error("Windows installer creation failed, aborting...") } } } } } } post { failure { echo "Build result FAILURE: Send email notification to jfaltermeier@eclipsesource.com" emailext attachLog: true, body: 'Job: ${JOB_NAME}
Build Number: ${BUILD_NUMBER}
Build URL: ${BUILD_URL}', mimeType: 'text/html', subject: 'Build ${JOB_NAME} (#${BUILD_NUMBER}) FAILURE', to: 'jfaltermeier@eclipsesource.com' } unstable { echo "Build result UNSTABLE: Send email notification to jfaltermeier@eclipsesource.com" emailext attachLog: true, body: 'Job: ${JOB_NAME}
Build Number: ${BUILD_NUMBER}
Build URL: ${BUILD_URL}', mimeType: 'text/html', subject: 'Build ${JOB_NAME} (#${BUILD_NUMBER}) UNSTABLE', to: 'jfaltermeier@eclipsesource.com' } } } def buildNext(boolean runTests, int sleepBetweenRetries) { int MAX_RETRY = 3 checkout scm // merge next branch into master to get any known fixes for next version // TODO there might be a more elegant way to merge a branch into this one // using a jenkings plugin from here sh "git config user.email \"not@real.user\"" sh "git config user.name \"Not a real user\"" sh "git fetch origin next" sh "git merge FETCH_HEAD" sh "node --version" // regular build sh "printenv && yarn cache dir" sh "yarn cache clean" try { sh(script: 'yarn --frozen-lockfile --force') } catch(error) { retry(MAX_RETRY) { sleep(sleepBetweenRetries) echo "yarn failed - Retrying" sh(script: 'yarn --frozen-lockfile --force') } } echo "Updating theia versions to next" sh "yarn update:next" try { sh(script: 'yarn --force') } catch(error) { retry(MAX_RETRY) { sleep(sleepBetweenRetries) echo "yarn failed - Retrying" sh(script: 'yarn --force') } } echo "Upgrading versions" sh "yarn upgrade" sh "git clean -xfd" try { sh(script: 'yarn --force') } catch(error) { retry(MAX_RETRY) { sleep(sleepBetweenRetries) echo "yarn failed - Retrying" sh(script: 'yarn --force') } } sh "rm -rf ./${distFolder}" sh "yarn build" sh "yarn download:plugins" sshagent(['projects-storage.eclipse.org-bot-ssh']) { sh "yarn electron package:preview" } if (runTests) { wrap([$class: 'Xvnc', takeScreenshot: false, useXauthority: true]) { sh 'yarn electron test' } } } ================================================ FILE: next/NEXT_INTEGRATION_BUILD.md ================================================ # Integration build against Theia@next The master branch has a script that may be used to update all theia versions to `next`. This may be executed running `yarn update:next` in the repository root. We will set up an integration job that will build the Eclipse Theia IDE against Theia@next so that we may identify breaking changes early. ## Build process * Check out the master branch containing all of the latest changes. * Merge the next branch into the checked out branch. The next branch will only contain fixes that are required to build against the `next` version (e.g. a method was renamed and causes compile errors) * Run `yarn update:next` (may have been exectued on `next` already but not necessarily) * Build the IDE and run the tests * In case of an error notify responsible persons via e-mail This process should make sure that we always include all of the latest changes (by using the master branch as a starting point for the build) and only have to maintain fixes (on the `next` branch) in case of actual problems. ## In case of errors * Analyse the issue and open a bug report * Either fix the issue (open a PR and merge to `next` branch) or create some kind of hotfix for the `next` branch to get a green build. ================================================ FILE: package.json ================================================ { "private": true, "version": "1.71.100", "license": "MIT", "author": "Rob Moran ", "homepage": "https://github.com/eclipse-theia/theia-ide#readme", "bugs": { "url": "https://github.com/eclipse-theia/theia/issues" }, "repository": { "type": "git", "url": "git+https://github.com/eclipse-theia/theia-ide.git" }, "engines": { "yarn": ">=1.7.0 <2", "node": ">=22" }, "devDependencies": { "@eclipse-dash/nodejs-wrapper": "^0.0.1", "@theia/cli": "1.72.0-next.20", "@types/yargs": "17.0.7", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/eslint-plugin-tslint": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "eslint-plugin-deprecation": "1.2.1", "eslint-plugin-import": "^2.32.0", "eslint-plugin-no-null": "^1.0.2", "eslint-plugin-no-unsanitized": "^3.2.0", "eslint-plugin-react": "^7.37.5", "lerna": "^9.0.7", "node-gyp": "^11.5.0", "rimraf": "^2.7.1", "ts-node": "^10.9.2", "type-fest": "^0.20.2", "yargs": "17.2.1" }, "scripts": { "clean": "lerna run clean && rimraf node_modules", "build": "yarn build:extensions && yarn build:applications", "build:dev": "yarn build:extensions && yarn build:applications:dev", "build:applications": "lerna run --scope=\"theia-ide*app\" --ignore=\"theia-ide-next*\" build:prod --concurrency 1", "build:applications:dev": "lerna run --scope=\"theia-ide*app\" --ignore=\"theia-ide-next*\" build --concurrency 1", "build:applications:next": "lerna run --scope=\"theia-ide-next*\" build:prod --concurrency 1", "build:applications:next:dev": "lerna run --scope=\"theia-ide-next*\" build --concurrency 1", "build:extensions": "lerna run --scope=\"theia-ide*ext\" build", "download:plugins": "theia download:plugins --rate-limit=15 --parallel=false && yarn permissions:writeable", "package:applications": "lerna run --scope=\"theia-ide*app\" --ignore=\"theia-ide-next*\" package --concurrency 1", "package:applications:preview": "lerna run --scope=\"theia-ide*app\" --ignore=\"theia-ide-next*\" package:preview --concurrency 1", "package:applications:prod": "lerna run --scope=\"theia-ide*app\" --ignore=\"theia-ide-next*\" package:prod --concurrency 1", "package:applications:next": "lerna run --scope=\"theia-ide-next*\" package --concurrency 1", "permissions:writeable": "ts-node scripts/make-files-writeable.ts plugins", "watch": "lerna run --parallel watch", "test": "lerna run test", "electron": "yarn --cwd applications/electron", "browser": "yarn --cwd applications/browser", "update:theia": "ts-node scripts/update-theia-version.ts", "update:theia:children": "lerna run update:theia -- ", "update:next": "ts-node scripts/update-theia-version.ts next && lerna run update:next", "lint": "eslint --ext js,jsx,ts,tsx scripts && lerna run lint", "lint:fix": "eslint --ext js,jsx,ts,tsx scripts --fix && lerna run lint:fix", "license:check": "npx dash-licenses-wrapper --configFile=./configs/license-check-config.json", "license:check:review": "npx dash-licenses-wrapper --configFile=./configs/license-check-config.json --review", "postinstall": "theia-patch && npx patch-package --patch-dir patches" }, "theiaPluginsDir": "plugins", "theiaPlugins": { "vscode-builtin-extensions": "https://github.com/eclipse-theia/vscode-builtin-extensions/releases/download/1.108.2/vscode-builtin-extensions-1.108.2.tar.gz", "vscjava.vscode-java-pack": "https://open-vsx.org/api/vscjava/vscode-java-pack/0.30.5/file/vscjava.vscode-java-pack-0.30.5.vsix", "vscjava.vscode-java-dependency": "https://open-vsx.org/api/vscjava/vscode-java-dependency/0.27.0/file/vscjava.vscode-java-dependency-0.27.0.vsix" }, "theiaPluginsExcludeIds": [ "ms-vscode.js-debug-companion", "VisualStudioExptTeam.vscodeintellicode", "vscode.extension-editing", "vscode.github", "vscode.github-authentication", "vscode.microsoft-authentication" ], "workspaces": [ "applications/*", "theia-extensions/*" ], "resolutions": { "@types/puppeteer": "^5.4.7", "@yarnpkg/parsers": "3.0.0-rc.44", "**/multer": "1.4.4-lts.1", "**/nan": "2.25.0", "**/cpu-features": "0.0.9", "**/perfect-scrollbar": "1.5.5", "**/node-abi": "4.14.0" } } ================================================ FILE: patches/@theia+terminal+1.72.0-next.20.patch ================================================ diff --git a/node_modules/@theia/terminal/lib/node/shell-integration-injector.js b/node_modules/@theia/terminal/lib/node/shell-integration-injector.js index 1234567..abcdefg 100644 --- a/node_modules/@theia/terminal/lib/node/shell-integration-injector.js +++ b/node_modules/@theia/terminal/lib/node/shell-integration-injector.js @@ -77,7 +77,7 @@ let ShellIntegrationInjector = class ShellIntegrationInjector { console.warn(`Shell integration file not found (application may not be bundled correctly): ${fullPath}`); return undefined; } - return fullPath; + return fullPath.replace('.asar' + path.sep, '.asar.unpacked' + path.sep); } stripLoginFlag(args) { if (args === undefined) { ================================================ FILE: releng/preview/Jenkinsfile.build ================================================ /** * This Jenkinsfile builds Theia across the major OS platforms * It's designed to be run only manually */ import groovy.json.JsonSlurper // Set permissions to allow the sign job to copy artifacts from this build properties([ copyArtifactPermission('theia-ide-sign-notarize'), copyArtifactPermission('theia-ide-upload') ]) pipeline { agent none // Store important values for later use environment { STORED_GIT_BRANCH = "${env.GIT_BRANCH ?: env.BRANCH_NAME ?: 'unknown'}" THEIA_IDE_JENKINS_CI = 'true' msvs_version = '2022' GYP_MSVS_VERSION = '2022' npm_config_msvs_version = '2022' NODE_OPTIONS = '--max_old_space_size=4096' } options { timeout(time: 5, unit: 'HOURS') disableConcurrentBuilds() durabilityHint('MAX_SURVIVABILITY') } parameters { booleanParam(name: 'DRY_RUN', defaultValue: true, description: 'If true, treat this as a dry run (no deployment of artifacts)') booleanParam(name: 'IS_PR', defaultValue: true, description: 'If true, treat this as a PR (only run this stage)') } stages { stage('Build') { parallel { stage('Linux: Create Installer') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: podRetention: never() containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "8000Mi" cpu: "2000m" requests: memory: "512Mi" cpu: "200m" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} """ } } steps { container('theia-dev') { withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { script { buildInstaller(120) } } } // Move Linux artifacts to linux folder for archiving sh "mkdir -p applications/electron/dist/linux" sh "find applications/electron/dist -maxdepth 1 -type f -not -path '*/\\.*' -exec mv {} applications/electron/dist/linux/ \\;" archiveArtifacts artifacts: "applications/electron/dist/linux/*", fingerprint: true } post { failure { error("Linux installer creation failed, aborting...") } } } stage('Mac: Create Installer') { options { skipDefaultCheckout true } agent { label 'macos' } steps { nodejs(nodeJSInstallationName: 'node_22.x') { script { sh "node --version" createMacInstaller() } } archiveArtifacts artifacts: "applications/electron/dist/**", fingerprint: true } post { failure { error("Mac installer creation failed, aborting...") } } } stage('Windows: Create Installer') { // Windows installer creation is split into two sub-stages running on // different agents: // 1. Build the unpacked electron app on a real Windows agent. // 2. Sign each internal `.exe` via the Eclipse Authenticode service and // package the NSIS installer from the already-signed directory on a // Linux k8s pod stages { stage('Windows: Build unpacked') { agent { label 'windows' } steps { nodejs(nodeJSInstallationName: 'node_24.x') { // analyze memory usage bat "wmic ComputerSystem get TotalPhysicalMemory" bat "wmic OS get FreePhysicalMemory" bat "tasklist" buildUnpackedWindows() } retry(3) { timeout(time: 30, unit: 'MINUTES') { stash includes: 'applications/electron/dist/win-unpacked/**', name: 'windows-unpacked' } } } } stage('Windows: Sign & Package') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: podRetention: never() containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "4000Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "200m" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} """ } } environment { WINEPREFIX = "${env.WORKSPACE}/.wine-prefix" } steps { checkout scm retry(3) { timeout(time: 30, unit: 'MINUTES') { unstash 'windows-unpacked' } } container('theia-dev') { sh ''' wine --version wine cmd /c echo wine-prefix-ok ''' withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { sh 'yarn --network-timeout 100000 --frozen-lockfile --force' sh 'yarn --cwd applications/electron sign:directory:windows -d dist/win-unpacked' // Generate app-update.yml for the Windows auto-updater. // electron-builder skips this when using --dir (step 1) because the // target is "dir", not "nsis", and --prepackaged (step 2) does not // re-run the afterPack lifecycle. sh 'node applications/electron/scripts/generate-app-update-yml.js' sh 'cd applications/electron && npx electron-builder --prepackaged dist/win-unpacked --win nsis -c.win.signAndEditExecutable=false --publish always' } } // Move Windows artifacts to windows folder for archiving sh "mkdir -p applications/electron/dist/windows" sh "find applications/electron/dist -maxdepth 1 -type f -not -path '*/\\.*' -exec mv {} applications/electron/dist/windows/ \\;" archiveArtifacts artifacts: "applications/electron/dist/windows/*", fingerprint: true } } } post { failure { error("Windows installer creation failed, aborting...") } } } } } } post { success { script { if (isPR()) { echo "This is a PR build. Skipping triggering of sign and notarize job." } else { echo "This is NOT a PR build. Preparing to trigger sign and notarize job." echo "DRY_RUN parameter value: ${params.DRY_RUN}" build job: 'theia-ide-sign-notarize', parameters: [ string(name: 'BUILD_NUMBER_PARAM', value: "${BUILD_NUMBER}"), booleanParam(name: 'DRY_RUN', value: params.DRY_RUN) ], wait: false echo "Triggered sign and notarize job successfully" } } } } } def detachVolume(String mountpoint) { try { sh "hdiutil detach \"${mountpoint}\" -force" } catch (Exception ex) { echo "Failed to detach ${mountpoint}: ${ex}" } } def createMacInstaller() { // Step 0: Checkout scm checkout scm // Step 1: Ensure directory is cleaned and recreated sh "rm -rf applications/electron/dist" sh "mkdir -p applications/electron/dist" // Step 2: Download and extract zip files for both architectures def architectures = ['mac-arm64', 'mac-x64'] architectures.each { arch -> sh "curl -L -o applications/electron/dist/${arch}.zip https://github.com/eclipse-theia/theia-ide/releases/download/pre-release/${arch}.zip" sh "unzip -o applications/electron/dist/${arch}.zip -d applications/electron/dist/${arch}" sh "rm applications/electron/dist/${arch}.zip" } sh "ls -al applications/electron/dist/mac-arm64 applications/electron/dist/mac-x64" // Step 3: Unpack DMG files for signing architectures.each { arch -> def mountPoint = "applications/electron/dist/${arch}/TheiaIDE-dmg-mounted" sh "rm -rf ${mountPoint}" sh "mkdir -p ${mountPoint}" sh "hdiutil attach applications/electron/dist/${arch}/TheiaIDE.dmg -mountpoint ${mountPoint}" // Create DMG layout structure sh "mkdir -p applications/electron/dist/${arch}/TheiaIDE-dmg-layout/.background" // Copy DS_Store if exists sh """ if [ -f ${mountPoint}/.DS_Store ]; then ditto ${mountPoint}/.DS_Store applications/electron/dist/${arch}/TheiaIDE-dmg-layout/ fi """ // Copy app and create Applications symlink sh "ditto ${mountPoint}/TheiaIDE.app applications/electron/dist/${arch}/TheiaIDE-dmg-layout/TheiaIDE.app" // Make sure the destination doesn't exist before creating the symlink sh "rm -f applications/electron/dist/${arch}/TheiaIDE-dmg-layout/Applications" sh "ln -sf /Applications applications/electron/dist/${arch}/TheiaIDE-dmg-layout/Applications" // Detach mounted DMG sh "hdiutil detach ${mountPoint}" // Step 4: Remove quarantine bits from all files sh "xattr -d -r com.apple.quarantine applications/electron/dist/${arch}/TheiaIDE-dmg-layout || true" } // Step 5: Sign binaries sh 'yarn --network-timeout 100000 --frozen-lockfile --force' sshagent(['projects-storage.eclipse.org-bot-ssh']) { architectures.each { arch -> def appPath = "/${pwd()}/applications/electron/dist/${arch}/TheiaIDE-dmg-layout/TheiaIDE.app" sh "yarn electron sign:directory -d \"${appPath}\"" } } // Step 6: Create the final DMG files architectures.each { arch -> sh "rm -f applications/electron/dist/${arch}/TheiaIDE.dmg" sh "hdiutil create -volname TheiaIDE -srcfolder applications/electron/dist/${arch}/TheiaIDE-dmg-layout -fs HFS+ -format UDZO applications/electron/dist/${arch}/TheiaIDE.dmg" sh "rm -rf applications/electron/dist/${arch}/TheiaIDE-dmg-layout" // Cleanup files we don't require sh "find applications/electron/dist/${arch} -type f ! -name \"TheiaIDE.dmg\" ! -name \"latest-mac.yml\" -delete" } sh "ls -al applications/electron/dist/mac-arm64 applications/electron/dist/mac-x64" } def buildUnpackedWindows() { checkout scm sh "git clean -xdf" sh "yarn cache clean" sh 'node --version' sh 'printenv && yarn cache dir' sh 'yarn --network-timeout 100000 --frozen-lockfile --force' sh 'yarn build:extensions' sh 'yarn electron build:prod' sh 'yarn download:plugins' sh 'cd applications/electron && npx electron-builder --dir --win -c.mac.identity=null --publish never' } def buildInstaller(int sleepBetweenRetries) { int maxRetry = 1 String buildPackageCmd checkout scm // Ensure build on a clean state sh "git clean -xdf" sh "yarn cache clean" sh 'node --version' sh 'printenv && yarn cache dir' // Execute build with retry capability try { // run install, build extensions and build electron app in separate steps sh 'yarn --network-timeout 100000 --frozen-lockfile --force' sh 'yarn build:extensions' sh 'yarn electron build:prod' } catch (error) { retry(maxRetry) { sleep(sleepBetweenRetries) echo 'yarn failed - Retrying' sh(script: buildPackageCmd) } } // Package the built application sshagent(['projects-storage.eclipse.org-bot-ssh']) { sh 'yarn download:plugins' sh 'yarn electron package:prod' } } // Helper function for dry run status def isDryRun() { return params.DRY_RUN } // Helper function for dPR status def isPR() { return params.IS_PR } ================================================ FILE: releng/preview/Jenkinsfile.sign ================================================ /** * This Jenkinsfile handles signing and notarizing Theia installers * It's designed to be run automatically after the build job */ import groovy.json.JsonSlurper // Set permissions to allow the upload job to copy artifacts from this build properties([ copyArtifactPermission('theia-ide-upload') ]) pipeline { agent none options { timeout(time: 5, unit: 'HOURS') disableConcurrentBuilds() durabilityHint('MAX_SURVIVABILITY') } parameters { string(name: 'BUILD_NUMBER_PARAM', defaultValue: '', description: 'The build number from the upstream build job') booleanParam(name: 'DRY_RUN', defaultValue: true, description: 'If true, treat this as a dry run (no deployment of artifacts)') } environment { THEIA_IDE_JENKINS_CI = 'true' msvs_version = '2019' GYP_MSVS_VERSION = '2019' NODE_OPTIONS = '--max_old_space_size=4096' } stages { stage('Sign and Notarize') { parallel { stage('Mac') { stages { stage('Mac: Sign and Notarize') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: podRetention: never() containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "4000Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "200m" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp - name: jnlp resources: limits: memory: "2000Mi" cpu: "1000m" requests: memory: "1024Mi" cpu: "250m" volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} - name: volume-known-hosts configMap: name: known-hosts """ } } steps { checkout scm // Clean and prepare target folder sh "rm -rf applications/electron/dist" sh "mkdir -p applications/electron/dist" echo "Fetching artifacts from 'theia-ide-release' build #${params.BUILD_NUMBER_PARAM}" // Retry copy artifacts with timeout to handle large files (up to 800 MiB) retry(3) { timeout(time: 30, unit: 'MINUTES') { copyArtifacts( projectName: "theia-ide-release", selector: specific("${params.BUILD_NUMBER_PARAM}"), filter: 'applications/electron/dist/**', target: '.', fingerprintArtifacts: true ) } } retry(2) { container('theia-dev') { withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { script { signInstaller('dmg', 'mac', 'mac-x64') notarizeInstaller('dmg', 'mac-x64') signInstaller('dmg', 'mac', 'mac-arm64') notarizeInstaller('dmg', 'mac-arm64') } } } } stash includes: "applications/electron/dist/mac-x64/**", name: 'mac' stash includes: "applications/electron/dist/mac-arm64/**", name: 'mac-arm' } } stage('Mac: Recreate Zip with Ditto for correct file permissions') { agent { label 'macos' } options { retry(2) } steps { checkout scm // Clean and prepare target folder sh "rm -rf applications/electron/dist" sh "mkdir -p applications/electron/dist" unstash 'mac' unstash 'mac-arm' script { def packageJSON = readJSON file: "package.json" String version = "${packageJSON.version}" def architectures = ['mac-x64', 'mac-arm64'] architectures.each { arch -> String targetFolder = "applications/electron/dist/${arch}" def notarizedDmg = "${targetFolder}/TheiaIDE.dmg" def mountPoint = "${targetFolder}/TheiaIDE-mount" def extractedFolder = "${targetFolder}/TheiaIDE-extracted" def rezippedFile = "${targetFolder}/TheiaIDE-rezipped.zip" def archSuffix = arch == 'mac-arm64' ? '-arm64' : '' def finalZip = "${targetFolder}/TheiaIDE-${version}${archSuffix}-mac.zip" // Clean and prepare sh "rm -rf \"${extractedFolder}\" \"${mountPoint}\"" sh "mkdir -p \"${extractedFolder}\" \"${mountPoint}\"" try { // Mount DMG sh "hdiutil attach \"${notarizedDmg}\" -mountpoint \"${mountPoint}\"" sleep 5 // Copy .app and check contents sh "ditto \"${mountPoint}/TheiaIDE.app\" \"${extractedFolder}/TheiaIDE.app\"" } finally { sh "hdiutil detach \"${mountPoint}\"" } // Create zip with ditto for proper permissions sh "ditto -c -k \"${extractedFolder}\" \"${rezippedFile}\"" sh "rm -f \"${finalZip}\"" sh "mv \"${rezippedFile}\" \"${finalZip}\"" // Cleanup sh "rm -rf \"${extractedFolder}\" \"${mountPoint}\"" } } archiveArtifacts artifacts: "applications/electron/dist/mac-x64/**", fingerprint: true archiveArtifacts artifacts: "applications/electron/dist/mac-arm64/**", fingerprint: true } } } } stage('Windows: Sign') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: podRetention: never() containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "2000Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "200m" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp - name: jnlp resources: limits: memory: "1000Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "250m" volumeMounts: - name: volume-known-hosts mountPath: /home/jenkins/.ssh volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} - name: volume-known-hosts configMap: name: known-hosts """ } } steps { checkout scm // Clean and prepare target folder sh "rm -rf applications/electron/dist" sh "mkdir -p applications/electron/dist" echo "Fetching artifacts from 'theia-ide-release' build #${params.BUILD_NUMBER_PARAM}" // Retry copy artifacts with timeout to handle large files (up to 800 MiB) retry(3) { timeout(time: 20, unit: 'MINUTES') { copyArtifacts( projectName: "theia-ide-release", selector: specific("${params.BUILD_NUMBER_PARAM}"), filter: 'applications/electron/dist/windows/*', fingerprintArtifacts: true ) } } container('theia-dev') { withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { script { signInstaller('exe', 'windows', 'windows') } } } // Archive signed Windows artifacts archiveArtifacts artifacts: "applications/electron/dist/windows/*", fingerprint: true } } } } } post { success { script { build job: 'theia-ide-upload', parameters: [ string(name: 'BUILD_NUMBER_PARAM', value: "${params.BUILD_NUMBER_PARAM}"), string(name: 'SIGN_NUMBER_PARAM', value: "${BUILD_NUMBER}"), booleanParam(name: 'DRY_RUN', value: params.DRY_RUN) ], wait: false } } } } def signInstaller(String ext, String os, String arch = '') { // Adjust the dist folder to include architecture if supplied String targetFolder = arch ? "applications/electron/dist/${arch}" : "applications/electron/dist" List installers = findFiles(glob: "${targetFolder}/*.${ext}") // Get the appropriate signing service URL String url if (os == 'mac') { url = 'https://cbi.eclipse.org/macos/codesign/sign' } else if (os == 'windows') { url = 'https://cbi.eclipse.org/authenticode/sign' } else { error("Error during signing: unsupported OS: ${os}") } if (installers.size() == 1) { sh "curl -o ${targetFolder}/signed-${installers[0].name} -F file=@${installers[0].path} ${url}" sh "rm ${installers[0].path}" sh "mv ${targetFolder}/signed-${installers[0].name} ${installers[0].path}" } else { error("Error during signing: installer not found or multiple installers exist: ${installers.size()}") } } def notarizeInstaller(String ext, String arch = '') { String service = 'https://cbi.eclipse.org/macos/xcrun' // Adjust the dist folder to include architecture if supplied String targetFolder = arch ? "applications/electron/dist/${arch}" : "applications/electron/dist" List installers = findFiles(glob: "${targetFolder}/*.${ext}") if (installers.size() != 1) { error("Error during notarization: installer not found or multiple installers exist: ${installers.size()}") } // Submit for notarization String response = sh( script: "curl -sS -X POST -F file=@${installers[0].path} -F 'options={\"primaryBundleId\":\"eclipse.theia\",\"staple\":true};type=application/json' ${service}/notarize", returnStdout: true ).trim() Map json = readJSON(text: response) String uuid = json.uuid as String String status = json.notarizationStatus?.status as String // Poll with timeout timeout(time: 20, unit: 'MINUTES') { waitUntil { echo "notarization status: ${status}" if (status == 'IN_PROGRESS') { sleep 60 response = sh(script: "curl -sS ${service}/${uuid}/status", returnStdout: true).trim() try { def poll = readJSON(text: response) status = poll.notarizationStatus?.status as String } catch (e) { error "Status returned non-JSON:\n${response}" } return false } return true } } if (status != 'COMPLETE') { error("Failed to notarize ${installers[0].name}: ${response}") } // Download notarized file sh "curl -sS -o '${targetFolder}/stapled-${installers[0].name}' ${service}/${uuid}/download" sh "rm '${installers[0].path}'" sh "mv '${targetFolder}/stapled-${installers[0].name}' '${installers[0].path}'" } ================================================ FILE: releng/preview/Jenkinsfile.upload ================================================ /** * This Jenkinsfile handles updating metadata and uploading the Theia installers * It's designed to be run automatically after the sign job */ import groovy.json.JsonSlurper pipeline { agent none options { timeout(time: 5, unit: 'HOURS') disableConcurrentBuilds() durabilityHint('MAX_SURVIVABILITY') } parameters { string(name: 'BUILD_NUMBER_PARAM', defaultValue: '', description: 'The build number from the upstream build job') string(name: 'SIGN_NUMBER_PARAM', defaultValue: '', description: 'The build number from the upstream sign job') booleanParam(name: 'DRY_RUN', defaultValue: true, description: 'If true, treat this as a dry run (no deployment of artifacts)') } environment { THEIA_IDE_JENKINS_CI = 'true' NODE_OPTIONS = '--max_old_space_size=4096' } stages { stage('Update and Upload') { parallel { stage('Linux: Upload') { agent any steps { checkout scm echo "Fetching Linux artifacts from build job #${params.BUILD_NUMBER_PARAM}" // Retry copy artifacts with timeout to handle large files (up to 800 MiB) retry(3) { timeout(time: 20, unit: 'MINUTES') { copyArtifacts( projectName: 'theia-ide-release', selector: specific("${params.BUILD_NUMBER_PARAM}"), filter: 'applications/electron/dist/linux/*', target: '.', fingerprintArtifacts: true ) } } script { uploadInstaller('linux', 'linux') } } } stage('Mac: Update Metadata and Upload') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: podRetention: never() containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "1700Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "200m" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp - name: jnlp resources: limits: memory: "2300Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "250m" volumeMounts: - name: volume-known-hosts mountPath: /home/jenkins/.ssh volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} - name: volume-known-hosts configMap: name: known-hosts """ } } steps { checkout scm echo "Fetching Mac artifacts from sign job #${params.SIGN_NUMBER_PARAM}" // Retry copy artifacts with timeout to handle large files (up to 800 MiB) retry(3) { timeout(time: 20, unit: 'MINUTES') { copyArtifacts( projectName: 'theia-ide-sign-notarize', selector: specific("${params.SIGN_NUMBER_PARAM}"), filter: 'applications/electron/dist/mac-x64/**', target: '.', fingerprintArtifacts: true ) } } container('theia-dev') { withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { script { def packageJSON = readJSON file: "package.json" String version = "${packageJSON.version}" updateMetadata('mac-x64/TheiaIDE-' + version + '-mac.zip', 'mac-x64/latest-mac.yml', 'macos', false, '.zip', 1200) updateMetadata('mac-x64/TheiaIDE.dmg', 'mac-x64/latest-mac.yml', 'macos', false, '.dmg', 1200) } } } container('jnlp') { script { uploadInstaller('macos', 'mac-x64') } } } } stage('Mac-Arm: Update Metadata and Upload') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: podRetention: never() containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "1700Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "200m" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp - name: jnlp resources: limits: memory: "2300Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "250m" volumeMounts: - name: volume-known-hosts mountPath: /home/jenkins/.ssh volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} - name: volume-known-hosts configMap: name: known-hosts """ } } steps { checkout scm echo "Fetching Mac-Arm artifacts from sign job #${params.SIGN_NUMBER_PARAM}" // Retry copy artifacts with timeout to handle large files (up to 800 MiB) retry(3) { timeout(time: 20, unit: 'MINUTES') { copyArtifacts( projectName: 'theia-ide-sign-notarize', selector: specific("${params.SIGN_NUMBER_PARAM}"), filter: 'applications/electron/dist/mac-arm64/**', target: '.', fingerprintArtifacts: true ) } } container('theia-dev') { withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { script { def packageJSON = readJSON file: "package.json" String version = "${packageJSON.version}" updateMetadata('mac-arm64/TheiaIDE-' + version + '-arm64-mac.zip', 'mac-arm64/latest-mac.yml', 'macos-arm', false, '.zip', 1200) updateMetadata('mac-arm64/TheiaIDE.dmg', 'mac-arm64/latest-mac.yml', 'macos-arm', false, '.dmg', 1200) } } } container('jnlp') { script { uploadInstaller('macos-arm', 'mac-arm64') } } } } stage('Windows: Update Metadata and Upload') { agent { kubernetes { yaml """ apiVersion: v1 kind: Pod spec: podRetention: never() containers: - name: theia-dev image: eclipsetheia/theia-blueprint:builder imagePullPolicy: Always command: - cat tty: true resources: limits: memory: "1700Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "200m" volumeMounts: - name: global-cache mountPath: /.cache - name: global-yarn mountPath: /.yarn - name: global-npm mountPath: /.npm - name: electron-cache mountPath: /.electron-gyp - name: jnlp resources: limits: memory: "1000Mi" cpu: "1000m" requests: memory: "512Mi" cpu: "250m" volumeMounts: - name: volume-known-hosts mountPath: /home/jenkins/.ssh volumes: - name: global-cache emptyDir: {} - name: global-yarn emptyDir: {} - name: global-npm emptyDir: {} - name: electron-cache emptyDir: {} - name: volume-known-hosts configMap: name: known-hosts """ } } steps { checkout scm echo "Fetching Windows artifacts from sign job #${params.SIGN_NUMBER_PARAM}" // Retry copy artifacts with timeout to handle large files (up to 800 MiB) retry(3) { timeout(time: 20, unit: 'MINUTES') { copyArtifacts( projectName: 'theia-ide-sign-notarize', selector: specific("${params.SIGN_NUMBER_PARAM}"), filter: 'applications/electron/dist/windows/*', target: '.', fingerprintArtifacts: true ) } } container('theia-dev') { withCredentials([string(credentialsId: "github-bot-token", variable: 'GITHUB_TOKEN')]) { script { updateMetadata('windows/TheiaIDESetup.exe', 'windows/latest.yml', 'windows', true, '.exe', 1200) } } } container('jnlp') { script { echo 'Computing updatable versions before uploading new installer' def updatableVersions = getUpdatableVersions() echo 'updatableVersions: ' + updatableVersions uploadInstaller('windows', 'windows') copyInstallerAndUpdateLatestYml('windows', 'TheiaIDESetup', 'exe', 'latest.yml', updatableVersions) } } } } } } } } def updateMetadata(String executable, String yaml, String platform, Boolean updatePaths, String fileExtension, int sleepBetweenRetries) { int maxRetry = 4 try { // Install dependencies and update metadata sh "yarn install --network-timeout 100000 --force" sh "yarn electron update:blockmap -e ${executable}" sh "yarn electron update:checksum -e ${executable} -y ${yaml} -p ${platform} -u ${updatePaths} -f ${fileExtension}" } catch (error) { retry(maxRetry) { sleep(sleepBetweenRetries) echo "yarn failed - Retrying" sh "yarn install --network-timeout 100000 --force" sh "yarn electron update:blockmap -e ${executable}" sh "yarn electron update:checksum -e ${executable} -y ${yaml} -p ${platform} -u ${updatePaths} -f ${fileExtension}" } } } def uploadInstaller(String platform, String folder = '') { if (!isDryRun()) { String targetFolder = folder ? "applications/electron/dist/${folder}" : "applications/electron/dist" def packageJSON = readJSON file: "package.json" String version = "${packageJSON.version}" sshagent(['projects-storage.eclipse.org-bot-ssh']) { // Remove and recreate version-specific folder sh "ssh genie.theia@projects-storage.eclipse.org rm -rf /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}" sh "ssh genie.theia@projects-storage.eclipse.org mkdir -p /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}" sh "scp ${targetFolder}/*.* genie.theia@projects-storage.eclipse.org:/home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}" // Remove and recreate latest folder sh "ssh genie.theia@projects-storage.eclipse.org rm -rf /home/data/httpd/download.eclipse.org/theia/ide-preview/latest/${platform}" sh "ssh genie.theia@projects-storage.eclipse.org mkdir -p /home/data/httpd/download.eclipse.org/theia/ide-preview/latest/${platform}" sh "scp ${targetFolder}/*.* genie.theia@projects-storage.eclipse.org:/home/data/httpd/download.eclipse.org/theia/ide-preview/latest/${platform}" } } else { echo "Skipped upload for dry run" echo "Would have uploaded the following artifacts" sh "ls -l applications/electron/dist/${folder}" } } /** * List all directories in the ide-preview directory that represent versions. * Only include version numbers lower than the current version. */ def getUpdatableVersions() { def packageJSON = readJSON file: "package.json" String currentVersion = "${packageJSON.version}" def versions = '' sshagent(['projects-storage.eclipse.org-bot-ssh']) { versions = sh( script: """ ssh genie.theia@projects-storage.eclipse.org "cd /home/data/httpd/download.eclipse.org/theia/ide-preview/ && \ find . -maxdepth 1 -type d -regex '.*/[0-9]+\\.[0-9]+\\.[0-9]+' -exec basename {} \\; | sort -V | awk -v curVer='${currentVersion}' '{ if (\\\$1 != curVer && \\\$1 < curVer) print \\\$1 }' | paste -sd ','" """, returnStdout: true ).trim() } return versions } /** * For Windows, create a versioned copy of the installer for updates * and update the latest.yml file for older versions. */ def copyInstallerAndUpdateLatestYml(String platform, String installer, String extension, String yaml, String updatableVersions) { if (!isDryRun()) { def packageJSON = readJSON file: "package.json" String version = "${packageJSON.version}" sshagent(['projects-storage.eclipse.org-bot-ssh']) { // Create versioned copies of installer and blockmap files sh "ssh genie.theia@projects-storage.eclipse.org cp /home/data/httpd/download.eclipse.org/theia/ide-preview/latest/${platform}/${installer}.${extension} /home/data/httpd/download.eclipse.org/theia/ide-preview/latest/${platform}/${installer}-${version}.${extension}" sh "ssh genie.theia@projects-storage.eclipse.org cp /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}/${installer}.${extension} /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}/${installer}-${version}.${extension}" sh "ssh genie.theia@projects-storage.eclipse.org cp /home/data/httpd/download.eclipse.org/theia/ide-preview/latest/${platform}/${installer}.${extension}.blockmap /home/data/httpd/download.eclipse.org/theia/ide-preview/latest/${platform}/${installer}-${version}.${extension}.blockmap" sh "ssh genie.theia@projects-storage.eclipse.org cp /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}/${installer}.${extension}.blockmap /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}/${installer}-${version}.${extension}.blockmap" } // Update latest.yml for older versions to enable auto-updates if (updatableVersions.length() > 0) { for (oldVersion in updatableVersions.split(",")) { sshagent(['projects-storage.eclipse.org-bot-ssh']) { sh "ssh genie.theia@projects-storage.eclipse.org rm -f /home/data/httpd/download.eclipse.org/theia/ide-preview/${oldVersion}/${platform}/${yaml}" sh "ssh genie.theia@projects-storage.eclipse.org cp /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}/${yaml} /home/data/httpd/download.eclipse.org/theia/ide-preview/${oldVersion}/${platform}/${yaml}" } } } else { echo "No updateable versions found" } } else { echo "Skipped copying installer for dry run" } } // Helper function for dry run status def isDryRun() { return params.DRY_RUN } ================================================ FILE: releng/promote/Jenkinsfile ================================================ /** * This Jenkinsfile promotes a given version of the Theia IDE from /theia/ide-preview to /theia/ide */ /* groovylint-disable NestedBlockDepth */ import groovy.json.JsonSlurper pipeline { agent none options { timeout(time: 3, unit: 'HOURS') disableConcurrentBuilds() } stages { stage('Setup parameters') { steps { script { properties([ parameters([ string( defaultValue: 'latest', name: 'VERSION', trim: true ) ]) ]) } } } stage('Promote') { agent any steps { script { promote('linux', params.VERSION) promote('macos', params.VERSION) promote('macos-arm', params.VERSION) promote('windows', params.VERSION) // update latest.yaml on windows for differential updater def updatableVersions = getUpdatableVersions(params.VERSION) echo 'updatableVersions: ' + updatableVersions updateLatestYaml('windows', params.VERSION, 'TheiaIDESetup', 'exe', 'latest.yml', updatableVersions) } } } } } def promote(String platform, String version) { sshagent(['projects-storage.eclipse.org-bot-ssh']) { sh "ssh genie.theia@projects-storage.eclipse.org rm -rf /home/data/httpd/download.eclipse.org/theia/ide/${version}/${platform}" sh "ssh genie.theia@projects-storage.eclipse.org mkdir -p /home/data/httpd/download.eclipse.org/theia/ide/${version}/${platform}" sh "ssh genie.theia@projects-storage.eclipse.org cp -a /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}/. /home/data/httpd/download.eclipse.org/theia/ide/${version}/${platform}/" sh "ssh genie.theia@projects-storage.eclipse.org rm -rf /home/data/httpd/download.eclipse.org/theia/ide/latest/${platform}" sh "ssh genie.theia@projects-storage.eclipse.org mkdir -p /home/data/httpd/download.eclipse.org/theia/ide/latest/${platform}" sh "ssh genie.theia@projects-storage.eclipse.org cp -a /home/data/httpd/download.eclipse.org/theia/ide-preview/${version}/${platform}/. /home/data/httpd/download.eclipse.org/theia/ide/latest/${platform}/" } } // copies updated (checksum, link to latest version) metadata yaml to older versions def updateLatestYaml(String platform, String version, String installer, String extension, String yaml, String UPDATABLE_VERSIONS) { if (UPDATABLE_VERSIONS.length() != 0) { for (oldVersion in UPDATABLE_VERSIONS.split(",")) { sshagent(['projects-storage.eclipse.org-bot-ssh']) { sh "ssh genie.theia@projects-storage.eclipse.org rm -f /home/data/httpd/download.eclipse.org/theia/ide/${oldVersion}/${platform}/${yaml}" sh "ssh genie.theia@projects-storage.eclipse.org cp /home/data/httpd/download.eclipse.org/theia/ide/${version}/${platform}/${yaml} /home/data/httpd/download.eclipse.org/theia/ide/${oldVersion}/${platform}/${yaml}" } } } else { echo "No updateable versions" } } /** * List all directories in the ide directory. * Only takes the ones with a version identifier name. * Only take version numbers lower than the current version. */ def getUpdatableVersions(String currentVersion) { def versions = '' sshagent(['projects-storage.eclipse.org-bot-ssh']) { versions = sh( script: """ ssh genie.theia@projects-storage.eclipse.org "cd /home/data/httpd/download.eclipse.org/theia/ide/ && \ find . -maxdepth 1 -type d -regex '.*/[0-9]+\\.[0-9]+\\.[0-9]+' -exec basename {} \\; | sort -V | awk -v curVer='${currentVersion}' '{ if (\\\$1 != curVer && \\\$1 < curVer) print \\\$1 }' | paste -sd ','" """, returnStdout: true ).trim() } return versions } ================================================ FILE: scripts/build-with-local-theia.js ================================================ #!/usr/bin/env node /** * Script to build Theia IDE with a local Theia framework development version. * * This allows testing Theia IDE changes against local Theia framework modifications * without publishing to npm first. It expects a locally cloned version of the * Theia repository (https://github.com/eclipse-theia/theia) that will be linked * into the Theia IDE build. * * Usage: * node scripts/build-with-local-theia.js [options] * * Options: * --theia-path Path to local Theia repository [default: ../theia] * --skip-theia-build Skip building Theia packages (use if already built) [default: false] * --skip-ide-build Skip building Theia IDE (use for linking only) [default: false] * --skip-plugins Skip downloading plugins [default: false] * --package Package the electron-next application after building [default: false] * --unlink Remove links and restore npm dependencies [default: false] * --dry-run Print commands without executing them [default: false] * --help Show this help message * * See docs/developing-with-local-theia.md for detailed documentation. */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const ROOT_DIR = path.resolve(__dirname, '..'); const DEFAULT_THEIA_PATH = path.resolve(ROOT_DIR, '..', 'theia'); // Parse command line arguments const args = process.argv.slice(2); function getArgValue(flag, defaultValue) { const index = args.indexOf(flag); if (index !== -1 && args[index + 1]) { return args[index + 1]; } return defaultValue; } const options = { theiaPath: path.resolve(getArgValue('--theia-path', DEFAULT_THEIA_PATH)), skipTheiaBuild: args.includes('--skip-theia-build'), skipIdeBuild: args.includes('--skip-ide-build'), skipPlugins: args.includes('--skip-plugins'), package: args.includes('--package'), unlink: args.includes('--unlink'), dryRun: args.includes('--dry-run'), help: args.includes('--help') }; if (options.help) { console.log(` Usage: node scripts/build-with-local-theia.js [options] This script builds the Theia IDE against a local Theia framework checkout, allowing you to test framework changes without publishing to npm first. The script uses yarn link to connect the local Theia packages to the IDE build. Note: This script does not update the IDE version or Theia package versions. It uses the current state of both repositories. If needed, you can run versioning commands (e.g., yarn update:theia) separately before building. Prerequisites: A local clone of the Theia repository (https://github.com/eclipse-theia/theia). By default, the script expects it at ../theia (next to the theia-ide directory). Options: --theia-path Path to local Theia repository [default: ../theia] --skip-theia-build Skip building Theia packages (use if already built) [default: false] --skip-ide-build Skip building Theia IDE (use for linking only) [default: false] --skip-plugins Skip downloading plugins [default: false] --package Package the electron-next application after building [default: false] --unlink Remove links and restore npm dependencies [default: false] --dry-run Print commands without executing them [default: false] --help Show this help message See docs/developing-with-local-theia.md for detailed documentation and examples. `); process.exit(0); } /** * Execute a shell command */ function run(cmd, cwd = ROOT_DIR, description = '') { if (description) { console.log(`\n${'='.repeat(60)}`); console.log(`> ${description}`); console.log(`${'='.repeat(60)}`); } console.log(`$ ${cmd}`); console.log(` (in ${cwd})\n`); if (options.dryRun) { console.log('[DRY RUN] Command not executed'); return ''; } try { execSync(cmd, { cwd, stdio: 'inherit', env: { ...process.env, NODE_OPTIONS: '--max_old_space_size=4096' } }); } catch (error) { console.error(`\nCommand failed: ${cmd}`); throw error; } } /** * Read JSON file */ function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } /** * Get all @theia/* packages from a package.json */ function getTheiaDependencies(packageJsonPath) { const pkg = readJson(packageJsonPath); const theiaDeps = new Set(); for (const deps of [pkg.dependencies, pkg.devDependencies]) { if (deps) { for (const dep of Object.keys(deps)) { if (dep.startsWith('@theia/')) { theiaDeps.add(dep); } } } } return theiaDeps; } /** * Find all Theia packages in the local Theia repository */ function findTheiaPackages(theiaPath) { const packagesDir = path.join(theiaPath, 'packages'); const devPackagesDir = path.join(theiaPath, 'dev-packages'); const packages = new Map(); for (const dir of [packagesDir, devPackagesDir]) { if (!fs.existsSync(dir)) { continue; } for (const entry of fs.readdirSync(dir)) { const pkgJsonPath = path.join(dir, entry, 'package.json'); if (fs.existsSync(pkgJsonPath)) { const pkg = readJson(pkgJsonPath); if (pkg.name && pkg.name.startsWith('@theia/')) { packages.set(pkg.name, path.join(dir, entry)); } } } } return packages; } /** * Collect all @theia/* dependencies from IDE packages */ function collectAllTheiaDependencies() { const allDeps = new Set(); // Root package.json const rootDeps = getTheiaDependencies(path.join(ROOT_DIR, 'package.json')); rootDeps.forEach(d => allDeps.add(d)); // Applications for (const app of ['electron', 'browser', 'electron-next']) { const appPkgPath = path.join(ROOT_DIR, 'applications', app, 'package.json'); if (fs.existsSync(appPkgPath)) { getTheiaDependencies(appPkgPath).forEach(d => allDeps.add(d)); } } // Extensions for (const ext of ['launcher', 'product', 'updater']) { const extPkgPath = path.join(ROOT_DIR, 'theia-extensions', ext, 'package.json'); if (fs.existsSync(extPkgPath)) { getTheiaDependencies(extPkgPath).forEach(d => allDeps.add(d)); } } return allDeps; } /** * Build Theia */ function buildTheia() { run('npm ci', options.theiaPath, 'Install Theia dependencies'); // Compile all Theia packages, no need to build the applications in this case run('npm run compile', options.theiaPath, 'Compile Theia packages'); } /** * Create yarn link for Theia packages */ function linkTheiaPackages() { console.log(`\n${'='.repeat(60)}`); console.log('> Creating yarn links for Theia packages'); console.log(`${'='.repeat(60)}\n`); const theiaPackages = findTheiaPackages(options.theiaPath); const requiredDeps = collectAllTheiaDependencies(); console.log(`Found ${theiaPackages.size} Theia packages`); console.log(`Theia IDE requires ${requiredDeps.size} @theia/* packages\n`); const linked = []; const missing = []; // Create links in Theia packages for (const dep of requiredDeps) { const pkgPath = theiaPackages.get(dep); if (pkgPath) { if (!options.dryRun) { console.log(`Linking ${dep}...`); try { execSync('yarn link', { cwd: pkgPath, stdio: 'pipe' }); } catch (e) { // Link might already exist, that's fine } } linked.push(dep); } else { missing.push(dep); } } if (options.dryRun) { console.log(`Would create yarn links for ${linked.length} packages`); } if (missing.length > 0) { console.log('\nWarning: The following packages were not found in local Theia:'); missing.forEach(m => console.log(` - ${m}`)); } // Link packages in IDE console.log(`\nLinking ${linked.length} packages in IDE...`); if (linked.length > 0) { const linkCmd = `yarn link ${linked.join(' ')}`; console.log(`$ ${linkCmd}`); if (!options.dryRun) { execSync(linkCmd, { cwd: ROOT_DIR, stdio: 'inherit' }); } else { console.log('[DRY RUN] Command not executed'); } } console.log(`\n${options.dryRun ? 'Would link' : 'Linked'} ${linked.length} packages`); return linked; } /** * Unlink Theia packages and restore npm dependencies */ function unlinkTheiaPackages() { console.log(`\n${'='.repeat(60)}`); console.log('> Removing yarn links and restoring npm dependencies'); console.log(`${'='.repeat(60)}\n`); const requiredDeps = collectAllTheiaDependencies(); // Unlink packages if (options.dryRun) { console.log(`Would unlink ${requiredDeps.size} packages`); } else { for (const dep of requiredDeps) { console.log(`Unlinking ${dep}...`); try { execSync(`yarn unlink ${dep}`, { cwd: ROOT_DIR, stdio: 'pipe' }); } catch (e) { // Ignore errors if not linked } } } // Reinstall to restore npm versions run('yarn --force', ROOT_DIR, 'Reinstall npm dependencies'); console.log('\nLinks removed and npm dependencies restored'); } /** * Build IDE */ function buildIde() { run('yarn', ROOT_DIR, 'Install IDE dependencies'); run('yarn build:extensions', ROOT_DIR, 'Build IDE extensions'); run('yarn build:applications:next:dev', ROOT_DIR, 'Build electron-next application'); if (!options.skipPlugins) { run('yarn download:plugins', ROOT_DIR, 'Download plugins'); } else { console.log('\nSkipping plugin download (--skip-plugins)'); } } /** * Package the electron-next application */ function packageApp() { run('yarn package:applications:next', ROOT_DIR, 'Package electron-next application'); } /** * Main function */ async function main() { console.log('\nBuild Theia IDE with local Theia framework\n'); console.log(`Theia path: ${options.theiaPath}`); const startTime = Date.now(); if (options.unlink) { unlinkTheiaPackages(); } else { // Verify Theia exists if (!fs.existsSync(options.theiaPath)) { console.error(`\nError: Theia directory not found at ${options.theiaPath}`); console.error('\nPlease clone the Theia repository first:'); console.error(' git clone https://github.com/eclipse-theia/theia.git ../theia'); console.error('\nOr specify a different location with --theia-path'); process.exit(1); } // Build Theia if (!options.skipTheiaBuild) { buildTheia(); } else { console.log('\nSkipping Theia build (--skip-theia-build)'); } // Link packages linkTheiaPackages(); // Build IDE if (!options.skipIdeBuild) { buildIde(); } else { console.log('\nSkipping IDE build (--skip-ide-build)'); } // Package if requested if (options.package) { packageApp(); } } const elapsed = ((Date.now() - startTime) / 1000 / 60).toFixed(2); console.log(`\n${'='.repeat(60)}`); console.log('Done!'); console.log(`${'='.repeat(60)}`); console.log(`Total time: ${elapsed} minutes`); if (!options.unlink && !options.package) { console.log(` Next steps: - Run the IDE: yarn --cwd applications/electron-next start - Make changes in Theia, rebuild: (cd ${options.theiaPath} && npm run compile) - Rebuild IDE: yarn build:applications:next:dev - Package the app: node scripts/build-with-local-theia.js --skip-theia-build --skip-ide-build --package - When done, restore npm deps: node scripts/build-with-local-theia.js --unlink `); } if (options.package) { console.log('\nPackaged application is in: applications/electron-next/dist/'); } if (options.dryRun) { console.log('\nThis was a dry run. No commands were actually executed.'); } } main().catch(error => { console.error('\nBuild failed:', error.message); process.exit(1); }); ================================================ FILE: scripts/generate-next-icons.js ================================================ #!/usr/bin/env node // @ts-check /** * Generates next icons by recoloring the Theia IDE blue (#00ADEE) to * next purple (#8B5CF6), matching the next splash screen color scheme. * White text and transparency are preserved. * * Source: applications/electron/resources/icons/ * Output: applications/electron-next/resources/icons/ * * Requires ImageMagick (`convert`). Falls back to `sharp` if ImageMagick is not available. * * Usage: node scripts/generate-next-icons.js */ const path = require('path'); const fs = require('fs'); const child_process = require('child_process'); const SOURCE_DIR = path.resolve(__dirname, '../applications/electron/resources/icons'); const TARGET_DIR = path.resolve(__dirname, '../applications/electron-next/resources/icons'); const ICON_MAPPINGS = [ { src: 'LinuxLauncherIcons/512x512.png', dest: 'LinuxLauncherIcons/512x512.png' }, { src: 'WindowIcon/512-512.png', dest: 'WindowIcon/512-512.png' }, { src: '512x512.png', dest: '512x512.png' }, ]; // Theia blue → next purple (same as splash screen logo) const THEIA_BLUE = '#00ADEE'; const NEXT_PURPLE = '#8B5CF6'; function hasImageMagick() { try { child_process.execSync('convert --version', { stdio: 'pipe' }); return true; } catch { return false; } } function generateWithImageMagick(srcPath, destPath) { const destDir = path.dirname(destPath); fs.mkdirSync(destDir, { recursive: true }); // Replace blue with purple on the RGB data, then re-apply original alpha. // The -fuzz 25% catches anti-aliased edge pixels near the blue color. const cmd = [ 'convert', `\\( "${srcPath}" -alpha off -fuzz 25% -fill "${NEXT_PURPLE}" -opaque "${THEIA_BLUE}" \\)`, `\\( "${srcPath}" -alpha extract \\)`, '-compose CopyOpacity -composite', `"${destPath}"` ].join(' '); child_process.execSync(cmd, { stdio: 'pipe' }); console.log(`Generated: ${destPath}`); } async function generateWithSharp(srcPath, destPath) { const sharp = require('sharp'); const destDir = path.dirname(destPath); fs.mkdirSync(destDir, { recursive: true }); const { data, info } = await sharp(srcPath) .ensureAlpha() .raw() .toBuffer({ resolveWithObject: true }); const { width, height, channels } = info; const output = Buffer.from(data); for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const i = (y * width + x) * channels; const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; // Compute saturation (HSV) to distinguish colored vs white pixels const max = Math.max(r, g, b); const min = Math.min(r, g, b); const sat = max === 0 ? 0 : (max - min) / max; if (sat > 0.10 && data[i + 3] > 0) { // Colored (blue) pixel: replace with next purple output[i] = 0x8B; output[i + 1] = 0x5C; output[i + 2] = 0xF6; } } } await sharp(output, { raw: { width, height, channels } }) .png() .toFile(destPath); console.log(`Generated: ${destPath}`); } async function main() { const useImageMagick = hasImageMagick(); if (!useImageMagick) { try { require('sharp'); } catch { console.error('Error: Neither ImageMagick nor sharp is available.'); console.error('Install ImageMagick: sudo apt-get install imagemagick'); console.error('Or install sharp: npm install sharp'); process.exit(1); } } console.log(`Using ${useImageMagick ? 'ImageMagick' : 'sharp'} for icon generation`); for (const mapping of ICON_MAPPINGS) { const srcPath = path.join(SOURCE_DIR, mapping.src); const destPath = path.join(TARGET_DIR, mapping.dest); if (!fs.existsSync(srcPath)) { console.warn(`Warning: Source icon not found: ${srcPath}`); continue; } if (useImageMagick) { generateWithImageMagick(srcPath, destPath); } else { await generateWithSharp(srcPath, destPath); } } console.log('Next icon generation complete!'); } main().catch(err => { console.error('Icon generation failed:', err); process.exit(1); }); ================================================ FILE: scripts/make-files-writeable.ts ================================================ /******************************************************************************** * Copyright (C) 2025 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import * as fs from 'fs'; import * as path from 'path'; const argv = yargs(hideBin(process.argv)) .option('directory', { alias: 'e', type: 'string', default: 'plugins', description: 'The parent directory which contains the files we need to make writable', }) .version(false) .wrap(120) .parseSync(); execute(); async function execute(): Promise { const directory = argv.directory; console.log(`Input directory: ${directory}`); try { makeWritable(directory); } catch (error) { console.error(`Failed to make files writable: ${error.message}`); process.exit(1); } } function makeWritable(dir: string): void { if (!fs.existsSync(dir)) { throw new Error(`Directory '${dir}' does not exist.`); } const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { makeWritable(fullPath); } else if (entry.isFile()) { const stats = fs.statSync(fullPath); const isWritable = (stats.mode & 0o200) !== 0; if (!isWritable) { const isExecutable = (stats.mode & 0o111) !== 0; const newMode = isExecutable ? 0o755 : 0o644; console.log(`Making '${fullPath}' writable with mode '${isExecutable ? '0o755' : '0o644'}'`); fs.chmodSync(fullPath, newMode); } } } } ================================================ FILE: scripts/update-theia-version.ts ================================================ /******************************************************************************** * Copyright (C) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import * as fs from 'fs'; import * as path from 'path'; import { PackageJson } from 'type-fest'; execute(); async function execute(): Promise { const theiaVersion = process.argv[2]; const packageJsonPath = path.resolve( './', 'package.json' ); console.log(`Updating ${packageJsonPath}...`); const packageJsonContents: string = fs.readFileSync(packageJsonPath, { encoding: 'utf8' }); const packageJson: PackageJson = JSON.parse(packageJsonContents); console.log('...dependencies...'); if (packageJson.dependencies) { updateTheiaVersions(packageJson.dependencies, theiaVersion); } console.log('...done...'); console.log('...devDependencies...'); if (packageJson.devDependencies) { updateTheiaVersions(packageJson.devDependencies, theiaVersion); } console.log('...done.'); // note: "null" is valid as per `stringify()` signature // eslint-disable-next-line no-null/no-null fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); } function updateTheiaVersions(dependencies: PackageJson.Dependency, theiaVersion: string): void { for (const dependency in dependencies) { if (dependency.startsWith('@theia/')) { console.log(`...setting ${dependency} from ${dependencies[dependency]} to next...`); dependencies[dependency] = theiaVersion; } } } ================================================ FILE: theia-extensions/launcher/.eslintrc.js ================================================ /** @type {import('eslint').Linter.Config} */ module.exports = { extends: [ '../../configs/build.eslintrc.json' ], parserOptions: { tsconfigRootDir: __dirname, project: 'tsconfig.json' } }; ================================================ FILE: theia-extensions/launcher/package.json ================================================ { "name": "theia-ide-launcher-ext", "version": "1.71.100", "keywords": [ "theia-extension" ], "license": "MIT", "repository": { "type": "git", "url": "https://github.com/eclipse-theia/theia-ide.git" }, "bugs": { "url": "https://github.com/eclipse-theia/theia-ide/issues" }, "homepage": "https://github.com/eclipse-theia/theia-ide", "files": [ "lib", "src" ], "dependencies": { "@theia/core": "1.72.0-next.20", "@vscode/sudo-prompt": "9.3.1", "body-parser": "^1.20.5", "fs-extra": "^4.0.3" }, "devDependencies": { "rimraf": "^2.7.1", "typescript": "^4.9.5" }, "scripts": { "clean": "rimraf lib *.tsbuildinfo", "build": "tsc -b", "lint": "eslint --ext js,jsx,ts,tsx src", "lint:fix": "eslint --ext js,jsx,ts,tsx src --fix", "watch": "tsc -w", "update:theia": "ts-node ../../scripts/update-theia-version.ts", "update:next": "ts-node ../../scripts/update-theia-version.ts next" }, "theiaExtensions": [ { "frontendElectron": "lib/browser/create-launcher-frontend-module", "backend": "lib/node/launcher-backend-module" } ] } ================================================ FILE: theia-extensions/launcher/src/browser/create-launcher-contribution.ts ================================================ /******************************************************************************** * Copyright (C) 2022-2024 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { ConfirmDialog, Dialog, FrontendApplication, FrontendApplicationContribution, StorageService } from '@theia/core/lib/browser'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { ILogger, MaybePromise } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common/nls'; import { inject, injectable } from '@theia/core/shared/inversify'; import { LauncherService } from './launcher-service'; import { DesktopFileService } from './desktopfile-service'; @injectable() export class CreateLauncherCommandContribution implements FrontendApplicationContribution { @inject(StorageService) protected readonly storageService: StorageService; @inject(ILogger) protected readonly logger: ILogger; @inject(LauncherService) private readonly launcherService: LauncherService; @inject(DesktopFileService) private readonly desktopFileService: DesktopFileService; onStart(_app: FrontendApplication): MaybePromise { const appConfig = FrontendApplicationConfigProvider.get(); const applicationName = appConfig.applicationName; const uriScheme = appConfig.electron.uriScheme; this.launcherService.isInitialized(uriScheme).then(async initialized => { if (!initialized) { const messageContainer = document.createElement('div'); // eslint-disable-next-line max-len messageContainer.textContent = nls.localizeByDefault(`Would you like to install a shell command that launches the application?\nYou will be able to run ${applicationName} from the command line by typing '${uriScheme}'.`); messageContainer.setAttribute('style', 'white-space: pre-line'); const details = document.createElement('p'); details.textContent = 'Administrator privileges are required, you will need to enter your password next.'; messageContainer.appendChild(details); const dialog = new ConfirmDialog({ title: nls.localizeByDefault('Create launcher'), msg: messageContainer, ok: Dialog.YES, cancel: Dialog.NO }); const install = await dialog.open(); this.launcherService.createLauncher(!!install, uriScheme); this.logger.info('Initialized application launcher.'); } else { this.logger.info('Application launcher was already initialized.'); } }); this.desktopFileService.isInitialized().then(async initialized => { if (!initialized) { const messageContainer = document.createElement('div'); // eslint-disable-next-line max-len messageContainer.textContent = nls.localizeByDefault(`Would you like to create a .desktop file for ${applicationName}?\nThis will make it easier to open ${applicationName} directly\nfrom your applications menu and enables further features.`); messageContainer.setAttribute('style', 'white-space: pre-line'); const dialog = new ConfirmDialog({ title: nls.localizeByDefault('Create .desktop file'), msg: messageContainer, ok: Dialog.YES, cancel: Dialog.NO }); const install = await dialog.open(); this.desktopFileService.createOrUpdateDesktopfile(!!install, { applicationName, createUrlHandler: true, uriScheme }); this.logger.info('Created or updated .desktop file.'); } else { this.logger.info('Desktop file was not updated or created.'); } }); } } ================================================ FILE: theia-extensions/launcher/src/browser/create-launcher-frontend-module.ts ================================================ /******************************************************************************** * Copyright (C) 2022-2024 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { CreateLauncherCommandContribution } from './create-launcher-contribution'; import { ContainerModule } from '@theia/core/shared/inversify'; import { LauncherService } from './launcher-service'; import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { DesktopFileService } from './desktopfile-service'; export default new ContainerModule(bind => { bind(FrontendApplicationContribution).to(CreateLauncherCommandContribution); bind(LauncherService).toSelf().inSingletonScope(); bind(DesktopFileService).toSelf().inSingletonScope(); }); ================================================ FILE: theia-extensions/launcher/src/browser/desktopfile-service.ts ================================================ /******************************************************************************** * Copyright (C) 2024 STMicroelectronics and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { Endpoint } from '@theia/core/lib/browser'; import { injectable } from '@theia/core/shared/inversify'; export interface DesktopFileOptions { applicationName?: string; createUrlHandler?: boolean; uriScheme?: string; } @injectable() export class DesktopFileService { async isInitialized(): Promise { const response = await fetch(new Request(`${this.endpoint()}/initialized`), { body: undefined, method: 'GET' }).then(r => r.json()); return !!response?.initialized; } async createOrUpdateDesktopfile(create: boolean, options?: DesktopFileOptions): Promise { fetch(new Request(`${this.endpoint()}`), { body: JSON.stringify({ create, ...options }), method: 'PUT', headers: new Headers({ 'Content-Type': 'application/json' }) }); } protected endpoint(): string { const url = new Endpoint({ path: 'desktopfile' }).getRestUrl().toString(); return url.endsWith('/') ? url.slice(0, -1) : url; } } ================================================ FILE: theia-extensions/launcher/src/browser/launcher-service.ts ================================================ /******************************************************************************** * Copyright (C) 2022 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { Endpoint } from '@theia/core/lib/browser'; import { injectable } from '@theia/core/shared/inversify'; @injectable() export class LauncherService { async isInitialized(uriScheme?: string): Promise { const query = uriScheme ? `?uriScheme=${encodeURIComponent(uriScheme)}` : ''; const response = await fetch(new Request(`${this.endpoint()}/initialized${query}`), { body: undefined, method: 'GET' }).then(r => r.json()); return !!response?.initialized; } async createLauncher(create: boolean, uriScheme?: string): Promise { fetch(new Request(`${this.endpoint()}`), { body: JSON.stringify({ create, uriScheme }), method: 'PUT', headers: new Headers({ 'Content-Type': 'application/json' }) }); } protected endpoint(): string { const url = new Endpoint({ path: 'launcher' }).getRestUrl().toString(); return url.endsWith('/') ? url.slice(0, -1) : url; } } ================================================ FILE: theia-extensions/launcher/src/node/desktopfile-endpoint.ts ================================================ /******************************************************************************** * Copyright (C) 2024 STMicroelectronics and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { Application, Router } from '@theia/core/shared/express'; import { inject, injectable } from '@theia/core/shared/inversify'; import { Request, Response } from 'express-serve-static-core'; import { json } from 'body-parser'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { getStorageFilePath } from './launcher-util'; import * as fs from 'fs-extra'; import * as path from 'path'; interface DesktopFileInformation { appImage: string; declined: string[]; } @injectable() export class TheiaDesktopFileServiceEndpoint implements BackendApplicationContribution { protected static PATH = '/desktopfile'; protected static STORAGE_FILE_NAME = 'desktopfile.json'; @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; configure(app: Application): void { const router = Router(); router.put('/', (request, response) => this.createOrUpdateDesktopfile(request, response)); router.get('/initialized', (request, response) => this.isInitialized(request, response)); app.use(json()); app.use(TheiaDesktopFileServiceEndpoint.PATH, router); } protected async isInitialized(_request: Request, response: Response): Promise { if (!process.env.APPIMAGE) { // we only want to create Desktop Files when running as an App Image response.json({ initialized: true }); } if (process.env.HOME === undefined) { // log error but assume initialized, since we can't proceed console.error('Desktop files can only be created if there is a set HOME directory'); response.json({ initialized: true }); } const storageFile = await getStorageFilePath(this.envServer, TheiaDesktopFileServiceEndpoint.STORAGE_FILE_NAME); if (!storageFile) { throw new Error('Could not resolve path to storage file.'); } if (!fs.existsSync(storageFile)) { response.json({ initialized: false }); return; } const appImageInformation = await this.readAppImageInformationFromStorage(storageFile); if (appImageInformation === undefined) { response.json({ initialized: false }); return; } if (appImageInformation.declined !== undefined && appImageInformation.declined.includes(process.env.APPIMAGE!)) { // we don't want to create Desktop Files for this App Image response.json({ initialized: true }); return; } const initialized = appImageInformation.appImage === process.env.APPIMAGE; response.json({ initialized }); } protected async readAppImageInformationFromStorage(storageFile: string): Promise { if (!fs.existsSync(storageFile)) { return undefined; } try { const data: DesktopFileInformation = await fs.readJSON(storageFile); return data; } catch (error) { console.error('Failed to parse data from "', storageFile, '". Reason:', error); return undefined; } } protected async createOrUpdateDesktopfile(request: Request, response: Response): Promise { const storageFile = await getStorageFilePath(this.envServer, TheiaDesktopFileServiceEndpoint.STORAGE_FILE_NAME); let appImageInformation: DesktopFileInformation | undefined = await this.readAppImageInformationFromStorage(storageFile); if (appImageInformation === undefined) { appImageInformation = { appImage: '', declined: [] }; } const createOrUpdate = request.body.create; const applicationName: string = request.body.applicationName || 'Theia IDE'; const createUrlHandler: boolean = request.body.createUrlHandler !== false; const uriScheme: string = request.body.uriScheme || 'theia'; const appId = applicationName.toLowerCase().replace(/\s+/g, '-'); if (createOrUpdate) { const iconFileName = appId + '-electron-app.png'; const applicationsDir = path.join(process.env.HOME!, '.local', 'share', 'applications'); const imagePath = path.join(applicationsDir, iconFileName); if (!fs.existsSync(imagePath)) { const appDir = process.env.APPDIR; if (appDir !== undefined) { let unpackedImagePath = path.join(appDir, iconFileName); if (!fs.existsSync(unpackedImagePath)) { // Fallback: find any .png icon in the AppImage root try { const pngFile = fs.readdirSync(appDir).find((f: string) => f.endsWith('.png')); if (pngFile) { unpackedImagePath = path.join(appDir, pngFile); } } catch { /* ignore */ } } if (fs.existsSync(unpackedImagePath)) { fs.copyFileSync(unpackedImagePath, imagePath); } else { console.warn('Launcher Icon not Found in App Image'); } } else { console.warn('Path for unpacked App Image not found'); } } const desktopFilePath = path.join(applicationsDir, `${appId}-launcher.desktop`); fs.outputFileSync(desktopFilePath, this.getDesktopFileContents(applicationName, process.env.APPIMAGE!, imagePath)); if (createUrlHandler) { const desktopURLFilePath = path.join(applicationsDir, `${appId}-launcher-url.desktop`); fs.outputFileSync(desktopURLFilePath, this.getDesktopURLFileContents(applicationName, process.env.APPIMAGE!, imagePath, uriScheme)); } appImageInformation.appImage = process.env.APPIMAGE!; fs.outputJSONSync(storageFile, appImageInformation); } else { appImageInformation.declined.push(process.env.APPIMAGE!); fs.outputJSONSync(storageFile, appImageInformation); } response.sendStatus(200); } protected getDesktopFileContents(applicationName: string, appImagePath: string, imagePath: string): string { return `[Desktop Entry] Name=${applicationName} GenericName=Integrated Development Environment Exec=${appImagePath} %U Terminal=false Type=Application Icon=${imagePath} StartupWMClass=${applicationName} Comment=IDE for cloud and desktop Categories=Development;IDE;`; } protected getDesktopURLFileContents(applicationName: string, appImagePath: string, imagePath: string, uriScheme: string = 'theia'): string { return `[Desktop Entry] Name=${applicationName} - URL Handler GenericName=Integrated Development Environment Exec=${appImagePath} --open-url %U Terminal=false Type=Application NoDisplay=true Icon=${imagePath} MimeType=x-scheme-handler/${uriScheme}; Comment=IDE for cloud and desktop Categories=Development;IDE;`; } } ================================================ FILE: theia-extensions/launcher/src/node/launcher-backend-module.ts ================================================ /******************************************************************************** * Copyright (C) 2022-2024 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { ContainerModule } from '@theia/core/shared/inversify'; import { TheiaLauncherServiceEndpoint } from './launcher-endpoint'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { TheiaDesktopFileServiceEndpoint } from './desktopfile-endpoint'; export default new ContainerModule(bind => { bind(TheiaLauncherServiceEndpoint).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(TheiaLauncherServiceEndpoint); bind(TheiaDesktopFileServiceEndpoint).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(TheiaDesktopFileServiceEndpoint); }); ================================================ FILE: theia-extensions/launcher/src/node/launcher-endpoint.ts ================================================ /******************************************************************************** * Copyright (C) 2022-2024 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { inject, injectable } from '@theia/core/shared/inversify'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { Application, Router, Request, Response } from '@theia/core/shared/express'; import { json } from 'body-parser'; import { ILogger } from '@theia/core/lib/common'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import * as sudo from '@vscode/sudo-prompt'; import * as fs from 'fs-extra'; import URI from '@theia/core/lib/common/uri'; import { getStorageFilePath } from './launcher-util'; interface PathEntry { source: string; target: string; } @injectable() export class TheiaLauncherServiceEndpoint implements BackendApplicationContribution { protected static PATH = '/launcher'; protected static STORAGE_FILE_NAME = 'paths.json'; @inject(ILogger) protected readonly logger: ILogger; @inject(EnvVariablesServer) protected readonly envServer: EnvVariablesServer; configure(app: Application): void { const router = Router(); router.put('/', (request, response) => this.createLauncher(request, response)); router.get('/initialized', (request, response) => this.isInitialized(request, response)); app.use(json()); app.use(TheiaLauncherServiceEndpoint.PATH, router); } private async isInitialized(request: Request, response: Response): Promise { if (!process.env.APPIMAGE) { // we are not running from an AppImage, so there's nothing to initialize // return true response.json({ initialized: true }); } const uriScheme = (request.query.uriScheme as string) || 'theia'; const launcherLink = `/usr/local/bin/${uriScheme}`; const storageFile = await getStorageFilePath(this.envServer, TheiaLauncherServiceEndpoint.STORAGE_FILE_NAME); if (!storageFile) { throw new Error('Could not resolve path to storage file.'); } if (!fs.existsSync(storageFile)) { response.json({ initialized: false }); return; } const data = await this.readLauncherPathsFromStorage(storageFile); const initialized = !!data.find(entry => entry.source === launcherLink); response.json({ initialized }); } private async readLauncherPathsFromStorage(storageFile: string): Promise { if (!fs.existsSync(storageFile)) { return []; } try { return await fs.readJSON(storageFile); } catch (error) { console.error('Failed to parse data from "', storageFile, '". Reason:', error); return []; } } private async getLogFilePath(): Promise { const configDirUri = await this.envServer.getConfigDirUri(); const logFileUri = new URI(configDirUri).resolve('logs/launcher.log'); return logFileUri.path.fsPath(); } private async createLauncher(request: Request, response: Response): Promise { const shouldCreateLauncher = request.body.create; const uriScheme: string = request.body.uriScheme || 'theia'; const launcher = `/usr/local/bin/${uriScheme}`; const sudoPromptName = uriScheme === 'theia-next' ? 'Theia IDE Next' : 'Theia IDE'; const target = process.env.APPIMAGE; const logFile = await this.getLogFilePath(); const command = `printf '%s\n' '#!/bin/bash' 'exec "${target}" \\$1 &> ${logFile} &' >${launcher} && chmod +x ${launcher}`; if (shouldCreateLauncher) { const targetExists = target && fs.existsSync(target); if (!targetExists) { throw new Error('Could not find application to launch'); } sudo.exec(command, { name: sudoPromptName }); } const storageFile = await getStorageFilePath(this.envServer, TheiaLauncherServiceEndpoint.STORAGE_FILE_NAME); const data = fs.existsSync(storageFile) ? await this.readLauncherPathsFromStorage(storageFile) : []; fs.outputJSONSync(storageFile, [...data, { source: launcher, target: shouldCreateLauncher ? target : undefined }]); response.sendStatus(200); } } ================================================ FILE: theia-extensions/launcher/src/node/launcher-util.ts ================================================ /******************************************************************************** * Copyright (C) 2024 STMicroelectronics and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import URI from '@theia/core/lib/common/uri'; export async function getStorageFilePath(envServer: EnvVariablesServer, fileName: string): Promise { const configDirUri = await envServer.getConfigDirUri(); const globalStorageFolderUri = new URI(configDirUri).resolve('globalStorage/theia-ide-launcher/' + fileName); const globalStorageFolderFsPath = globalStorageFolderUri.path.fsPath(); return globalStorageFolderFsPath; } ================================================ FILE: theia-extensions/launcher/tsconfig.json ================================================ { "extends": "../../configs/base.tsconfig", "compilerOptions": { "rootDir": "src", "outDir": "lib", "baseUrl": ".", "esModuleInterop": true }, "include": ["src"] } ================================================ FILE: theia-extensions/product/.eslintrc.js ================================================ /** @type {import('eslint').Linter.Config} */ module.exports = { extends: [ '../../configs/build.eslintrc.json' ], parserOptions: { tsconfigRootDir: __dirname, project: 'tsconfig.json' } }; ================================================ FILE: theia-extensions/product/package.json ================================================ { "private": true, "name": "theia-ide-product-ext", "version": "1.71.100", "description": "Eclipse Theia IDE Product Branding", "dependencies": { "@theia/core": "1.72.0-next.20", "@theia/getting-started": "1.72.0-next.20", "@theia/vsx-registry": "1.72.0-next.20", "@theia/workspace": "1.72.0-next.20", "inversify": "^6.2.2" }, "devDependencies": { "rimraf": "^2.7.1", "tslint": "^5.20.1", "typescript": "^4.9.5" }, "theiaExtensions": [ { "frontend": "lib/browser/theia-ide-frontend-module", "electronMain": "lib/electron-main/theia-ide-main-module" } ], "keywords": [ "theia-extension" ], "license": "MIT", "repository": { "type": "git", "url": "https://github.com/eclipse-theia/theia-ide.git" }, "bugs": { "url": "https://github.com/eclipse-theia/theia-ide/issues" }, "homepage": "https://github.com/eclipse-theia/theia-ide", "files": [ "lib", "src" ], "scripts": { "clean": "rimraf lib *.tsbuildinfo", "build": "tsc -b", "lint": "eslint --ext js,jsx,ts,tsx src", "lint:fix": "eslint --ext js,jsx,ts,tsx src --fix", "update:theia": "ts-node ../../scripts/update-theia-version.ts", "update:next": "ts-node ../../scripts/update-theia-version.ts next" }, "peerDependencies": { "react": "^16.8.0" } } ================================================ FILE: theia-extensions/product/src/browser/branding-util.tsx ================================================ /******************************************************************************** * Copyright (C) 2020 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { WindowService } from '@theia/core/lib/browser/window/window-service'; import * as React from 'react'; import { getBrandingVariant } from './theia-ide-config'; export interface ExternalBrowserLinkProps { text: string; url: string; windowService: WindowService; } export function renderProductName(): React.ReactNode { const variant = getBrandingVariant(); const suffix = variant !== 'stable' ? ` ${variant.charAt(0).toUpperCase() + variant.slice(1)}` : ''; return

Eclipse Theia IDE{suffix}

; } function BrowserLink(props: ExternalBrowserLinkProps): JSX.Element { return {props.text} ; } export function renderWhatIs(windowService: WindowService): React.ReactNode { return

What is this?

The Eclipse Theia IDE is a modern and open IDE for cloud and desktop. The Theia IDE is based on the .
The IDE is available as a . You can also . The online test version is limited to 30 minutes per session and hosted via .
; } export function renderExtendingCustomizing(windowService: WindowService): React.ReactNode { return

Extending/Customizing the Theia IDE

You can extend the Theia IDE at runtime by installing VS Code extensions, e.g. from the , an open marketplace for VS Code extensions. Just open the extension view or browse .
Furthermore, the Theia IDE is based on the flexible Theia platform. Therefore, the Theia IDE can serve as a template for building custom tools and IDEs. Browse to help you customize and build your own Eclipse Theia-based product.
; } export function renderSupport(windowService: WindowService): React.ReactNode { return

Professional Support

Professional support, implementation services, consulting and training for building tools like Theia IDE and for building other tools based on Eclipse Theia is available by selected companies as listed on the .
; } export function renderTickets(windowService: WindowService): React.ReactNode { return

Reporting feature requests and bugs

The features in the Eclipse Theia IDE are based on Theia and the included extensions/plugins. For bugs in Theia please consider opening an issue in the .
Eclipse Theia IDE only packages existing functionality into a product and installers for the product. If you believe there is a mistake in packaging, something needs to be added to the packaging or the installers do not work properly, please to let us know.
; } export function renderSourceCode(windowService: WindowService): React.ReactNode { return

Source Code

The source code of Eclipse Theia IDE is available on .
; } export function renderDocumentation(windowService: WindowService): React.ReactNode { return

Documentation

Please see the on how to use the Theia IDE.
; } export function renderCollaboration(windowService: WindowService): React.ReactNode { return

Collaboration

The IDE features a built-in collaboration feature. You can share your workspace with others and work together in real-time by clicking on the Collaborate item in the status bar. The collaboration feature is powered by the project and uses their public server infrastructure.
; } export function renderDownloads(): React.ReactNode { return

Updates and Downloads

You can update Eclipse Theia IDE directly in this application by navigating to File {'>'} Preferences {'>'} Check for Updates… Moreover the application will check for updates after each launch automatically.
Alternatively you can download the most recent version from the download page.
; } ================================================ FILE: theia-extensions/product/src/browser/style/index.css ================================================ /******************************************************************************** * Copyright (C) 2020 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ :root { --theia-branding-logo: url(../icons/TheiaIDE.png); } .theia-icon { background-image: url("../icons/512-512.png"); background-position: center; background-repeat: no-repeat; background-size: contain; } body[data-theia-branding="next"] { --theia-branding-logo: url(../icons/TheiaIDE-next.png); } body[data-theia-branding="next"] .theia-icon { background-image: url("../icons/512-512-next.png"); } .gs-blue-header { color: #5088e7; text-transform: capitalize; font-weight: 600; } .gs-text-bold { font-weight: 600; } .gs-text-underline { text-decoration: underline; } .gs-float { float: right; padding-left: 20px; } .gs-logo { background-image: var(--theia-branding-logo); background-position: center center; background-repeat: no-repeat; background-size: contain; width: 250px; height: 118px; padding: 20px; } .ad-logo { background-image: var(--theia-branding-logo); background-position: center center; background-repeat: no-repeat; background-size: contain; width: 250px; height: 118px; padding: 20px; } .ad-float { float: right; } .ad-container { padding: 20px; width: 1150px; height: 700; } ul.theia-aboutExtensions { height: 450px; overflow: hidden; overflow-y: scroll; list-style-type: none; padding: 0; margin-left: 10px; } ================================================ FILE: theia-extensions/product/src/browser/theia-ide-about-dialog.tsx ================================================ /******************************************************************************** * Copyright (C) 2020 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import * as React from 'react'; import { AboutDialog, AboutDialogProps, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog'; import { injectable, inject } from '@theia/core/shared/inversify'; import { renderDocumentation, renderDownloads, renderProductName, renderSourceCode, renderSupport, renderTickets, renderWhatIs } from './branding-util'; import { VSXEnvironment } from '@theia/vsx-registry/lib/common/vsx-environment'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; @injectable() export class TheiaIDEAboutDialog extends AboutDialog { @inject(VSXEnvironment) protected readonly environment: VSXEnvironment; @inject(WindowService) protected readonly windowService: WindowService; protected vscodeApiVersion: string; constructor( @inject(AboutDialogProps) protected readonly props: AboutDialogProps ) { super(props); } protected async doInit(): Promise { this.vscodeApiVersion = await this.environment.getVscodeApiVersion(); super.doInit(); } protected render(): React.ReactNode { return
{this.renderContent()}
; } protected renderContent(): React.ReactNode { return
{this.renderExtensions()}
{this.renderTitle()}
{renderWhatIs(this.windowService)}
{renderSupport(this.windowService)}
{renderTickets(this.windowService)}
{renderSourceCode(this.windowService)}
{renderDocumentation(this.windowService)}
{renderDownloads()}
; } protected renderTitle(): React.ReactNode { return
{renderProductName()} {this.renderVersion()}
; } protected renderVersion(): React.ReactNode { return

{this.applicationInfo ? 'Version ' + this.applicationInfo.version : '-'}

{'VS Code API Version: ' + this.vscodeApiVersion}

; } } ================================================ FILE: theia-extensions/product/src/browser/theia-ide-config.ts ================================================ /******************************************************************************** * Copyright (C) 2026 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; export type BrandingVariant = 'stable' | 'next'; export function getBrandingVariant(): BrandingVariant { try { const config = FrontendApplicationConfigProvider.get() as Record; return (config['brandingVariant'] as BrandingVariant) ?? 'stable'; } catch { return 'stable'; } } export function applyBranding(): void { const variant = getBrandingVariant(); if (variant !== 'stable') { document.body.setAttribute('data-theia-branding', variant); } } ================================================ FILE: theia-extensions/product/src/browser/theia-ide-contribution.tsx ================================================ /******************************************************************************** * Copyright (C) 2021 Ericsson and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { inject, injectable } from '@theia/core/shared/inversify'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; import { MenuContribution, MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; export namespace TheiaIDEMenus { export const THEIA_IDE_HELP: MenuPath = [...CommonMenus.HELP, 'theia-ide']; } export namespace TheiaIDECommands { export const CATEGORY = 'TheiaIDE'; export const REPORT_ISSUE: Command = { id: 'theia-ide:report-issue', category: CATEGORY, label: 'Report Issue' }; export const DOCUMENTATION: Command = { id: 'theia-ide:documentation', category: CATEGORY, label: 'Documentation' }; } @injectable() export class TheiaIDEContribution implements CommandContribution, MenuContribution { @inject(WindowService) protected readonly windowService: WindowService; static REPORT_ISSUE_URL = 'https://github.com/eclipse-theia/theia-ide/issues/new?assignees=&labels=&template=bug_report.md'; static DOCUMENTATION_URL = 'https://theia-ide.org/docs/user_getting_started/'; registerCommands(commandRegistry: CommandRegistry): void { commandRegistry.registerCommand(TheiaIDECommands.REPORT_ISSUE, { execute: () => this.windowService.openNewWindow(TheiaIDEContribution.REPORT_ISSUE_URL, { external: true }) }); commandRegistry.registerCommand(TheiaIDECommands.DOCUMENTATION, { execute: () => this.windowService.openNewWindow(TheiaIDEContribution.DOCUMENTATION_URL, { external: true }) }); } registerMenus(menus: MenuModelRegistry): void { menus.registerMenuAction(TheiaIDEMenus.THEIA_IDE_HELP, { commandId: TheiaIDECommands.REPORT_ISSUE.id, label: TheiaIDECommands.REPORT_ISSUE.label, order: '1' }); menus.registerMenuAction(TheiaIDEMenus.THEIA_IDE_HELP, { commandId: TheiaIDECommands.DOCUMENTATION.id, label: TheiaIDECommands.DOCUMENTATION.label, order: '2' }); } } ================================================ FILE: theia-extensions/product/src/browser/theia-ide-frontend-module.ts ================================================ /******************************************************************************** * Copyright (C) 2020 TypeFox, EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import '../../src/browser/style/index.css'; import { WidgetFactory } from '@theia/core/lib/browser'; import { AboutDialog } from '@theia/core/lib/browser/about-dialog'; import { applyBranding } from './theia-ide-config'; import { CommandContribution } from '@theia/core/lib/common/command'; import { ContainerModule } from '@theia/core/shared/inversify'; import { GettingStartedWidget } from '@theia/getting-started/lib/browser/getting-started-widget'; import { MenuContribution } from '@theia/core/lib/common/menu'; import { TheiaIDEAboutDialog } from './theia-ide-about-dialog'; import { TheiaIDEContribution } from './theia-ide-contribution'; import { TheiaIDEGettingStartedWidget } from './theia-ide-getting-started-widget'; export default new ContainerModule((bind, _unbind, isBound, rebind) => { applyBranding(); bind(TheiaIDEGettingStartedWidget).toSelf(); bind(WidgetFactory).toDynamicValue(context => ({ id: GettingStartedWidget.ID, createWidget: () => context.container.get(TheiaIDEGettingStartedWidget), })).inSingletonScope(); if (isBound(AboutDialog)) { rebind(AboutDialog).to(TheiaIDEAboutDialog).inSingletonScope(); } else { bind(AboutDialog).to(TheiaIDEAboutDialog).inSingletonScope(); } bind(TheiaIDEContribution).toSelf().inSingletonScope(); [CommandContribution, MenuContribution].forEach(serviceIdentifier => bind(serviceIdentifier).toService(TheiaIDEContribution) ); }); ================================================ FILE: theia-extensions/product/src/browser/theia-ide-getting-started-widget.tsx ================================================ /******************************************************************************** * Copyright (C) 2020 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import * as React from 'react'; import { Message } from '@theia/core/lib/browser'; import { PreferenceService } from '@theia/core/lib/common'; import { inject, injectable } from '@theia/core/shared/inversify'; import { renderDocumentation, renderDownloads, renderExtendingCustomizing, renderProductName, renderSourceCode, renderSupport, renderTickets, renderWhatIs, renderCollaboration } from './branding-util'; import { GettingStartedWidget } from '@theia/getting-started/lib/browser/getting-started-widget'; import { VSXEnvironment } from '@theia/vsx-registry/lib/common/vsx-environment'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; @injectable() export class TheiaIDEGettingStartedWidget extends GettingStartedWidget { @inject(VSXEnvironment) protected readonly environment: VSXEnvironment; @inject(WindowService) protected readonly windowService: WindowService; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; protected vscodeApiVersion: string; protected async doInit(): Promise { super.doInit(); this.vscodeApiVersion = await this.environment.getVscodeApiVersion(); await this.preferenceService.ready; this.update(); } protected onActivateRequest(msg: Message): void { super.onActivateRequest(msg); const htmlElement = document.getElementById('alwaysShowWelcomePage'); if (htmlElement) { htmlElement.focus(); } } protected render(): React.ReactNode { return
{this.renderActions()}
{this.renderHeader()}
{this.renderNews()}
{renderWhatIs(this.windowService)}
{renderExtendingCustomizing(this.windowService)}
{renderSupport(this.windowService)}
{renderTickets(this.windowService)}
{renderSourceCode(this.windowService)}
{renderDocumentation(this.windowService)}
{this.renderAIBanner()}
{renderCollaboration(this.windowService)}
{renderDownloads()}
{this.renderPreferences()}
; } protected renderActions(): React.ReactNode { return
{this.renderStart()}
{this.renderRecentWorkspaces()}
{this.renderSettings()}
{this.renderHelp()}
; } protected renderHeader(): React.ReactNode { return
{renderProductName()} {this.renderVersion()}
; } protected renderVersion(): React.ReactNode { return

{this.applicationInfo ? 'Version ' + this.applicationInfo.version : '-'}

{'VS Code API Version: ' + this.vscodeApiVersion}

; } protected renderAIBanner(): React.ReactNode { const framework = super.renderAIBanner(); if (React.isValidElement, HTMLDivElement>>(framework)) { return React.cloneElement(framework, { className: 'gs-section' }); } return framework; } } ================================================ FILE: theia-extensions/product/src/electron-main/icon-contribution.ts ================================================ /******************************************************************************** * Copyright (C) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import * as os from 'os'; import * as path from 'path'; import { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; import { injectable } from '@theia/core/shared/inversify'; import { BrowserWindow } from '@theia/core/electron-shared/electron'; @injectable() export class IconContribution implements ElectronMainApplicationContribution { onStart(application: ElectronMainApplication): void { if (os.platform() === 'linux') { const windowOptions = application.config.electron.windowOptions; if (windowOptions && windowOptions.icon === undefined) { // The window image is undefined. If the executable has an image set, this is used as a fallback. // Since AppImage does not support this anymore via electron-builder, set an image for the linux platform. windowOptions.icon = path.join(__dirname, '../../resources/icons/WindowIcon/512-512.png'); // also update any existing windows, e.g. the splashscreen for (const window of BrowserWindow.getAllWindows()) { window.setIcon(path.join(__dirname, '../../resources/icons/WindowIcon/512-512.png')); } } } } } ================================================ FILE: theia-extensions/product/src/electron-main/theia-ide-main-module.ts ================================================ /******************************************************************************** * Copyright (C) 2021 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { ContainerModule } from '@theia/core/shared/inversify'; import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; import { IconContribution } from './icon-contribution'; export default new ContainerModule(bind => { bind(IconContribution).toSelf().inSingletonScope(); bind(ElectronMainApplicationContribution).toService(IconContribution); }); ================================================ FILE: theia-extensions/product/tsconfig.json ================================================ { "extends": "../../configs/base.tsconfig", "compilerOptions": { "rootDir": "src", "outDir": "lib", "baseUrl": ".", "esModuleInterop": true }, "include": [ "src", ] } ================================================ FILE: theia-extensions/updater/.eslintrc.js ================================================ /** @type {import('eslint').Linter.Config} */ module.exports = { extends: [ '../../configs/build.eslintrc.json' ], parserOptions: { tsconfigRootDir: __dirname, project: 'tsconfig.json' } }; ================================================ FILE: theia-extensions/updater/package.json ================================================ { "private": true, "name": "theia-ide-updater-ext", "version": "1.71.100", "description": "Eclipse Theia IDE Updater", "dependencies": { "@theia/core": "1.72.0-next.20", "@theia/output": "1.72.0-next.20", "@theia/preferences": "1.72.0-next.20", "builder-util-runtime": "9.3.1", "electron-log": "^4.4.8", "electron-updater": "6.6.2", "fs-extra": "^10.1.0", "vscode-uri": "^2.1.2" }, "devDependencies": { "rimraf": "^2.7.1", "tslint": "^5.20.1", "typescript": "^4.9.5" }, "theiaExtensions": [ { "electronMain": "lib/electron-main/update/theia-updater-main-module", "frontendElectron": "lib/electron-browser/theia-updater-frontend-module" } ], "keywords": [ "theia-extension" ], "license": "MIT", "repository": { "type": "git", "url": "https://github.com/eclipse-theia/theia-ide.git" }, "bugs": { "url": "https://github.com/eclipse-theia/theia-ide/issues" }, "homepage": "https://github.com/eclipse-theia/theia-ide", "files": [ "lib", "src" ], "scripts": { "clean": "rimraf lib *.tsbuildinfo", "build": "tsc -b", "lint": "eslint --ext js,jsx,ts,tsx src", "lint:fix": "eslint --ext js,jsx,ts,tsx src --fix", "update:theia": "ts-node ../../scripts/update-theia-version.ts", "update:next": "ts-node ../../scripts/update-theia-version.ts next" } } ================================================ FILE: theia-extensions/updater/src/common/updater/theia-updater.ts ================================================ /******************************************************************************** * Copyright (C) 2020 TypeFox, EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { RpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; export const TheiaUpdaterPath = '/services/theia-updater'; export const TheiaUpdater = Symbol('TheiaUpdater'); export interface UpdaterSettings { checkForUpdates: boolean; checkInterval: number; channel: 'stable' | 'preview' | 'next'; } export interface TheiaUpdater extends RpcServer { checkForUpdates(): void; downloadUpdate(): void; onRestartToUpdateRequested(): void; disconnectClient(client: TheiaUpdaterClient): void; cancel(): void; setUpdaterSettings(settings: UpdaterSettings): void; } export const TheiaUpdaterClient = Symbol('TheiaUpdaterClient'); export interface UpdaterError { message: string; errorLogPath?: string; } export interface UpdateInfo { version: string; } export interface UpdateAvailabilityInfo { available: boolean; updateInfo?: UpdateInfo; } export interface TheiaUpdaterClient { updateAvailable(available: boolean, updateInfo?: UpdateInfo): void; notifyReadyToInstall(): void; reportError(error: UpdaterError): void; reportCancelled(): void; } ================================================ FILE: theia-extensions/updater/src/electron-browser/theia-updater-frontend-module.ts ================================================ /******************************************************************************** * Copyright (C) 2020 TypeFox, EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; import { ElectronMenuUpdater, TheiaUpdaterClientImpl, TheiaUpdaterFrontendContribution } from './updater/theia-updater-frontend-contribution'; import { TheiaUpdater, TheiaUpdaterClient, TheiaUpdaterPath } from '../common/updater/theia-updater'; import { ContainerModule } from '@theia/core/shared/inversify'; import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source'; import { PreferenceContribution } from '@theia/core/lib/common'; import { theiaUpdaterPreferenceSchema } from './updater/theia-updater-preferences'; export default new ContainerModule((bind, _unbind, isBound, rebind) => { bind(ElectronMenuUpdater).toSelf().inSingletonScope(); bind(TheiaUpdaterClientImpl).toSelf().inSingletonScope(); bind(TheiaUpdaterClient).toService(TheiaUpdaterClientImpl); bind(TheiaUpdater).toDynamicValue(context => { const client = context.container.get(TheiaUpdaterClientImpl); return ElectronIpcConnectionProvider.createProxy(context.container, TheiaUpdaterPath, client); }).inSingletonScope(); bind(TheiaUpdaterFrontendContribution).toSelf().inSingletonScope(); bind(MenuContribution).toService(TheiaUpdaterFrontendContribution); bind(CommandContribution).toService(TheiaUpdaterFrontendContribution); bind(PreferenceContribution).toConstantValue({ schema: theiaUpdaterPreferenceSchema }); }); ================================================ FILE: theia-extensions/updater/src/electron-browser/updater/theia-updater-frontend-contribution.ts ================================================ /******************************************************************************** * Copyright (C) 2020 TypeFox, EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { Command, CommandContribution, CommandRegistry, Emitter, MenuContribution, MenuModelRegistry, MenuPath, MessageService, Progress } from '@theia/core/lib/common'; import { PreferenceScope, PreferenceService } from '@theia/core/lib/common'; import { TheiaUpdater, TheiaUpdaterClient, UpdaterError, UpdateInfo, UpdateAvailabilityInfo, UpdaterSettings } from '../../common/updater/theia-updater'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { CommonMenus, OpenerService } from '@theia/core/lib/browser'; import { ElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; import URI from '@theia/core/lib/common/uri'; import { URI as VSCodeURI } from 'vscode-uri'; export namespace TheiaUpdaterCommands { const category = 'Theia Electron Updater'; export const CHECK_FOR_UPDATES: Command = { id: 'electron-theia:check-for-updates', label: 'Check for Updates...', category }; export const RESTART_TO_UPDATE: Command = { id: 'electron-theia:restart-to-update', label: 'Restart to Update', category }; } export namespace TheiaUpdaterMenu { export const MENU_PATH: MenuPath = [...CommonMenus.FILE_SETTINGS_SUBMENU, '3_settings_submenu_update']; } @injectable() export class TheiaUpdaterClientImpl implements TheiaUpdaterClient { protected readonly onReadyToInstallEmitter = new Emitter(); readonly onReadyToInstall = this.onReadyToInstallEmitter.event; protected readonly onUpdateAvailableEmitter = new Emitter(); readonly onUpdateAvailable = this.onUpdateAvailableEmitter.event; protected readonly onErrorEmitter = new Emitter(); readonly onError = this.onErrorEmitter.event; protected readonly onCancelEmitter = new Emitter(); readonly onCancel = this.onCancelEmitter.event; notifyReadyToInstall(): void { this.onReadyToInstallEmitter.fire(); } updateAvailable(available: boolean, updateInfo?: UpdateInfo): void { this.onUpdateAvailableEmitter.fire({ available, updateInfo }); } reportError(error: UpdaterError): void { this.onErrorEmitter.fire(error); } reportCancelled(): void { this.onCancelEmitter.fire(); } } // Dynamic menus aren't yet supported by electron: https://github.com/eclipse-theia/theia/issues/446 @injectable() export class ElectronMenuUpdater { @inject(ElectronMainMenuFactory) protected readonly factory: ElectronMainMenuFactory; public update(): void { this.setMenu(); } private setMenu(): void { window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar()); } } @injectable() export class TheiaUpdaterFrontendContribution implements CommandContribution, MenuContribution { @inject(MessageService) protected readonly messageService: MessageService; @inject(ElectronMenuUpdater) protected readonly menuUpdater: ElectronMenuUpdater; @inject(TheiaUpdater) protected readonly updater: TheiaUpdater; @inject(TheiaUpdaterClientImpl) protected readonly updaterClient: TheiaUpdaterClientImpl; @inject(PreferenceService) private readonly preferenceService: PreferenceService; @inject(OpenerService) protected readonly openerService: OpenerService; protected readyToUpdate = false; private progress: Progress | undefined; private intervalId: NodeJS.Timeout | undefined; private currentUpdateInfo: UpdateInfo | undefined; @postConstruct() protected init(): void { this.updaterClient.onUpdateAvailable(({ available, updateInfo }) => { if (available) { this.currentUpdateInfo = updateInfo; this.handleDownloadUpdate(updateInfo); } else { this.handleNoUpdate(); } }); this.updaterClient.onReadyToInstall(async () => { this.readyToUpdate = true; this.menuUpdater.update(); this.handleUpdatesAvailable(); }); this.updaterClient.onError(error => this.handleError(error)); this.updaterClient.onCancel(() => this.stopProgress()); this.preferenceService.ready.then(() => { this.syncUpdaterSettings(); }); this.preferenceService.onPreferenceChanged(e => { if (e.preferenceName === 'updates.checkForUpdates' || e.preferenceName === 'updates.checkInterval' || e.preferenceName === 'updates.channel') { this.syncUpdaterSettings(); } }); } protected syncUpdaterSettings(): void { const settings: UpdaterSettings = { checkForUpdates: this.preferenceService.get('updates.checkForUpdates', true), checkInterval: this.preferenceService.get('updates.checkInterval', 60), channel: this.preferenceService.get<'stable' | 'preview' | 'next'>('updates.channel', 'stable') }; this.updater.setUpdaterSettings(settings); } registerCommands(registry: CommandRegistry): void { registry.registerCommand(TheiaUpdaterCommands.CHECK_FOR_UPDATES, { execute: async () => { this.updater.checkForUpdates(); }, isEnabled: () => !this.readyToUpdate, isVisible: () => !this.readyToUpdate }); registry.registerCommand(TheiaUpdaterCommands.RESTART_TO_UPDATE, { execute: () => this.updater.onRestartToUpdateRequested(), isEnabled: () => this.readyToUpdate, isVisible: () => this.readyToUpdate }); } registerMenus(registry: MenuModelRegistry): void { registry.registerMenuAction(TheiaUpdaterMenu.MENU_PATH, { commandId: TheiaUpdaterCommands.CHECK_FOR_UPDATES.id }); registry.registerMenuAction(TheiaUpdaterMenu.MENU_PATH, { commandId: TheiaUpdaterCommands.RESTART_TO_UPDATE.id }); } protected async handleDownloadUpdate(updateInfo?: UpdateInfo): Promise { const message = updateInfo ? `Update to version ${updateInfo.version} found, do you want to update?` : 'Updates found, do you want to update?'; const actions = ['Not now', 'Yes']; const checkForUpdates = this.preferenceService.get('updates.checkForUpdates', true); if (checkForUpdates) { actions.push('Never'); } const answer = await this.messageService.info(message, ...actions); if (answer === 'Never') { this.preferenceService.set('updates.checkForUpdates', false, PreferenceScope.User); return; } if (answer === 'Yes') { this.stopProgress(); this.progress = await this.messageService.showProgress({ text: 'Theia IDE Update', options: { cancelable: true } }, () => this.updater.cancel()); let dots = 0; this.intervalId = setInterval(() => { if (this.progress !== undefined) { dots = (dots + 1) % 4; this.progress.report({ message: 'Downloading' + '.'.repeat(dots) }); } }, 1000); this.updater.downloadUpdate(); } } protected async handleNoUpdate(): Promise { this.messageService.info('Already using the latest version'); } protected async handleUpdatesAvailable(): Promise { if (this.progress !== undefined) { this.progress.report({ work: { done: 1, total: 1 } }); this.stopProgress(); } const message = this.currentUpdateInfo ? `An update to version ${this.currentUpdateInfo.version} has been downloaded and will be automatically installed on exit. Do you want to restart now?` : 'An update has been downloaded and will be automatically installed on exit. Do you want to restart now?'; const answer = await this.messageService.info(message, 'No', 'Yes'); if (answer === 'Yes') { this.updater.onRestartToUpdateRequested(); } } protected async handleError(error: UpdaterError): Promise { this.stopProgress(); if (error.errorLogPath) { const viewLogAction = 'View Error Log'; const answer = await this.messageService.error(error.message, viewLogAction); if (answer === viewLogAction) { const uri = new URI(VSCodeURI.file(error.errorLogPath)); const opener = await this.openerService.getOpener(uri); opener.open(uri); } } else { this.messageService.error(error.message); } } private stopProgress(): void { if (this.intervalId !== undefined) { clearInterval(this.intervalId); this.intervalId = undefined; } if (this.progress !== undefined) { this.progress.cancel(); this.progress = undefined; } } } ================================================ FILE: theia-extensions/updater/src/electron-browser/updater/theia-updater-preferences.ts ================================================ /******************************************************************************** * Copyright (C) 2020 EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { PreferenceSchema, PreferenceScope } from '@theia/core'; import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; const DEFAULT_UPDATE_CHANNELS = ['stable', 'preview']; function getAvailableUpdateChannels(): string[] { try { const config = FrontendApplicationConfigProvider.get() as Record; return (config['availableUpdateChannels'] as string[]) ?? DEFAULT_UPDATE_CHANNELS; } catch { return DEFAULT_UPDATE_CHANNELS; } } export const theiaUpdaterPreferenceSchema: PreferenceSchema = { 'properties': { 'updates.checkForUpdates': { type: 'boolean', description: 'Automatically check for updates.', default: true, scope: PreferenceScope.User }, 'updates.checkInterval': { type: 'number', description: 'Interval in minutes between automatic update checks.', default: 60, scope: PreferenceScope.User }, 'updates.channel': { type: 'string', enum: getAvailableUpdateChannels(), description: 'Channel to use for updates.', default: getAvailableUpdateChannels()[0] ?? '', scope: PreferenceScope.User }, } }; ================================================ FILE: theia-extensions/updater/src/electron-main/update/theia-updater-impl.ts ================================================ /******************************************************************************** * Copyright (C) 2020 TypeFox, EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import * as fs from 'fs-extra'; import * as http from 'http'; import * as os from 'os'; import * as path from 'path'; import { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; import { TheiaUpdater, TheiaUpdaterClient, UpdaterSettings } from '../../common/updater/theia-updater'; import { injectable } from '@theia/core/shared/inversify'; import { isOSX, isWindows } from '@theia/core'; import { CancellationToken } from 'builder-util-runtime'; const STABLE_CHANNEL_WINDOWS = 'https://download.eclipse.org/theia/ide/version/windows'; const STABLE_CHANNEL_MACOS = 'https://download.eclipse.org/theia/ide/latest/macos'; const STABLE_CHANNEL_MACOS_ARM = 'https://download.eclipse.org/theia/ide/latest/macos-arm'; const STABLE_CHANNEL_LINUX = 'https://download.eclipse.org/theia/ide/latest/linux'; const PREVIEW_CHANNEL_WINDOWS = 'https://download.eclipse.org/theia/ide-preview/version/windows'; const PREVIEW_CHANNEL_MACOS = 'https://download.eclipse.org/theia/ide-preview/latest/macos'; const PREVIEW_CHANNEL_MACOS_ARM = 'https://download.eclipse.org/theia/ide-preview/latest/macos-arm'; const PREVIEW_CHANNEL_LINUX = 'https://download.eclipse.org/theia/ide-preview/latest/linux'; // Next updates are currently only available for Linux. // The feed is served from GitHub Release assets (rolling "next" tag). const NEXT_CHANNEL_LINUX = 'https://github.com/eclipse-theia/theia-ide/releases/download/next'; const { autoUpdater } = require('electron-updater'); autoUpdater.logger = require('electron-log'); autoUpdater.logger.transports.file.level = 'info'; @injectable() export class TheiaUpdaterImpl implements TheiaUpdater, ElectronMainApplicationContribution { protected clients: Array = []; protected settings: UpdaterSettings = { checkForUpdates: true, checkInterval: 60, channel: 'stable' }; private initialCheck: boolean = true; private reportOnFirstRegistration: boolean = false; private cancellationToken: CancellationToken = new CancellationToken(); private updateCheckTimer: NodeJS.Timeout | undefined; constructor() { autoUpdater.autoDownload = false; autoUpdater.on('update-available', (info: { version: string }) => { if (this.initialCheck) { this.initialCheck = false; if (this.clients.length === 0) { this.reportOnFirstRegistration = true; } } const updateInfo = { version: info.version }; this.clients.forEach(c => c.updateAvailable(true, updateInfo)); }); autoUpdater.on('update-not-available', () => { if (this.initialCheck) { this.initialCheck = false; return; } this.clients.forEach(c => c.updateAvailable(false)); }); autoUpdater.on('update-downloaded', () => { this.clients.forEach(c => c.notifyReadyToInstall()); }); autoUpdater.on('error', (err: unknown) => { if (err instanceof Error && err.message.includes('cancelled')) { return; } const errorLogPath = autoUpdater.logger.transports.file.getFile().path; this.clients.forEach(c => c.reportError({ message: 'An error has occurred while attempting to update.', errorLogPath })); }); } checkForUpdates(): void { const feedURL = this.getFeedURL(this.settings.channel); autoUpdater.setFeedURL(feedURL); autoUpdater.checkForUpdates(); } setUpdaterSettings(settings: UpdaterSettings): void { const settingsChanged = this.settings.checkForUpdates !== settings.checkForUpdates || this.settings.checkInterval !== settings.checkInterval || this.settings.channel !== settings.channel; this.settings = settings; if (settingsChanged) { this.scheduleUpdateChecks(); } } onRestartToUpdateRequested(): void { autoUpdater.quitAndInstall(); } cancel(): void { autoUpdater.logger.info('Update cancelled by user'); this.cancellationToken.cancel(); this.clients.forEach(c => c.reportCancelled()); } downloadUpdate(): void { autoUpdater.logger.info('Downloading update'); this.cancellationToken = new CancellationToken(); autoUpdater.downloadUpdate(this.cancellationToken); // record download stat, ignore errors fs.mkdtemp(path.join(os.tmpdir(), 'updater-')) .then(tmpDir => { const file = fs.createWriteStream(path.join(tmpDir, 'update')); http.get('https://www.eclipse.org/downloads/download.php?file=/theia/update&r=1', response => { response.pipe(file); file.on('finish', () => { file.close(); }); }); }); } onStart(application: ElectronMainApplication): void { } onStop(application: ElectronMainApplication): void { this.stopUpdateCheckTimer(); } private scheduleUpdateChecks(): void { this.stopUpdateCheckTimer(); if (!this.settings.checkForUpdates) { return; } this.checkForUpdates(); const intervalMs = Math.max(this.settings.checkInterval, 1) * 60 * 1000; this.updateCheckTimer = setInterval(() => { if (this.settings.checkForUpdates) { this.checkForUpdates(); } }, intervalMs); } private stopUpdateCheckTimer(): void { if (this.updateCheckTimer) { clearInterval(this.updateCheckTimer); this.updateCheckTimer = undefined; } } setClient(client: TheiaUpdaterClient | undefined): void { if (client) { this.clients.push(client); if (this.reportOnFirstRegistration) { this.reportOnFirstRegistration = false; this.clients.forEach(c => c.updateAvailable(true)); } } } protected getFeedURL(channel: string): string { if (isWindows) { const curVersion = autoUpdater.currentVersion.toString(); // Next not yet available on Windows, fall back to stable return (channel === 'preview') ? PREVIEW_CHANNEL_WINDOWS.replace('version', curVersion) : STABLE_CHANNEL_WINDOWS.replace('version', curVersion); } else if (isOSX) { // Next not yet available on macOS, fall back to stable if (process.arch === 'arm64') { return (channel === 'preview') ? PREVIEW_CHANNEL_MACOS_ARM : STABLE_CHANNEL_MACOS_ARM; } else { return (channel === 'preview') ? PREVIEW_CHANNEL_MACOS : STABLE_CHANNEL_MACOS; } } else { if (channel === 'next') { return NEXT_CHANNEL_LINUX; } return (channel === 'preview') ? PREVIEW_CHANNEL_LINUX : STABLE_CHANNEL_LINUX; } } disconnectClient(client: TheiaUpdaterClient): void { const index = this.clients.indexOf(client); if (index !== -1) { this.clients.splice(index, 1); } } dispose(): void { this.stopUpdateCheckTimer(); this.clients.forEach(this.disconnectClient.bind(this)); } } ================================================ FILE: theia-extensions/updater/src/electron-main/update/theia-updater-main-module.ts ================================================ /******************************************************************************** * Copyright (C) 2020 TypeFox, EclipseSource and others. * * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. * * SPDX-License-Identifier: MIT ********************************************************************************/ import { TheiaUpdater, TheiaUpdaterClient, TheiaUpdaterPath } from '../../common/updater/theia-updater'; import { ContainerModule } from '@theia/core/shared/inversify'; import { ElectronConnectionHandler } from '@theia/core/lib/electron-main/messaging/electron-connection-handler'; import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; import { JsonRpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory'; import { TheiaUpdaterImpl } from './theia-updater-impl'; export default new ContainerModule(bind => { bind(TheiaUpdaterImpl).toSelf().inSingletonScope(); bind(TheiaUpdater).toService(TheiaUpdaterImpl); bind(ElectronMainApplicationContribution).toService(TheiaUpdater); bind(ElectronConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(TheiaUpdaterPath, client => { const server = context.container.get(TheiaUpdater); server.setClient(client); client.onDidCloseConnection(() => server.disconnectClient(client)); return server; }) ).inSingletonScope(); }); ================================================ FILE: theia-extensions/updater/tsconfig.json ================================================ { "extends": "../../configs/base.tsconfig", "compilerOptions": { "rootDir": "src", "outDir": "lib", "baseUrl": ".", "esModuleInterop": true }, "include": [ "src", ] } ================================================ FILE: tsconfig.json ================================================ { "extends": "./configs/base.tsconfig.json", "include": [], "compilerOptions": { "composite": true, "allowJs": true }, "references": [ { "path": "theia-extensions/launcher" }, { "path": "theia-extensions/product" }, { "path": "theia-extensions/updater" }, { "path": "applications/electron" }, { "path": "applications/browser" }, ] }