Repository: chenhunghan/lens-ext-invaders Branch: main Commit: 230dfd2005f0 Files: 20 Total size: 30.0 KB Directory structure: gitextract_hjnyx861/ ├── .eslintrc ├── .github/ │ └── workflows/ │ └── pr_release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── components/ │ ├── Alien.ts │ ├── AlienBullet.ts │ ├── Bullet.ts │ ├── ClusterPage.tsx │ ├── Game.tsx │ ├── Invaders.ts │ ├── Particle.ts │ ├── Player.ts │ └── PlayerBullet.ts ├── main.ts ├── package.json ├── renderer.tsx ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { // `root: true` prevent eslint to combine .eslintrc in parent folder with this one // ext developer can remove this line if desire to let eslint to traverses eslint config // see "root": true, // require to install @typescript-eslint/parser + typescript "parser": "@typescript-eslint/parser", // Tells ESLint to load the plugin. this allows you to use // the rules within your codebase "plugins": [ "@typescript-eslint", "react-hooks" ], "extends": [ // ESLint's inbuilt "recommended" config "eslint:recommended", // require to install @typescript-eslint/eslint-plugin // just like eslint:recommended, except it only turns on rules from TypeScript-specific plugin "plugin:@typescript-eslint/recommended" ], "env": { // this tells eslint it's safe to use `require`/`module.exports`/`__dirname`... // that are only legal in node.js, we use these in `webpack.config.js` "node": true, "jest": true }, "rules": { "indent": ["error", 2], "linebreak-style": ["error", "unix"], "quotes": ["error", "double"], "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies } } ================================================ FILE: .github/workflows/pr_release.yaml ================================================ on: pull_request: types: [closed] jobs: versioning: name: Auto versioning/tag => Create a release with .tgz # github does not have PR event type 'merged' for github action # so we use 'closed' instead, and add condition: if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token fetch-depth: 0 # otherwise, you will failed to push refs to dest repo # same node.js version as in package.json > engines - uses: actions/setup-node@v2-beta with: node-version: '12' # npm version patch will automatically git commit/tag - name: Run 'npm version patch' run: | git config --local user.email "github-action@github.com" git config --local user.name "Github Action Bot" npm version patch - name: Push commits uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.ref }} tags: true - name: Install dependencies and build extension run: | yarn yarn build - name: Create tarball from package run: | npm pack - name: Get version id: version run: | VERSION=$(node -p -e "require('./package.json').version") echo "::set-output name=value::$VERSION" - name: Create GitHub release uses: actions/create-release@v1 id: create_release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: "v${{ steps.version.outputs.value }}" release_name: "v${{ steps.version.outputs.value }}" draft: false - name: Upload GitHub assets uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: lensapp-lens-ext-invaders-${{ steps.version.outputs.value }}.tgz asset_name: lensapp-lens-ext-invaders-${{ steps.version.outputs.value }}.tgz asset_content_type: application/octet-stream ================================================ FILE: .gitignore ================================================ node_modules dist .DS_Store lens.log *.tgz ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 chh 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: README.md ================================================ # Start Invading Clusters ⚠️⚠️⚠️ Please don't play this game on your production cluster. 😅 Alien 👾 = pod on cluster. ⚠️⚠️⚠️ ![screencast](screencast_256.gif) ## Install ```bash cd ~ && git clone https://github.com/chenhunghan/lens-ext-invaders.git cd lens-ext-invaders && yarn && yarn build ln -s ~/lens-ext-invaders ~/.k8slens/extensions/lens-ext-invaders ``` OR Copy the `.tgz` link in [Release](https://github.com/chenhunghan/lens-ext-invaders/releases) Add install the extension by paste the link in Lens -> Extension ![intstall](https://i.imgur.com/oFzyvGG.png) ## Acknowledgements This project is largely inspired by ================================================ FILE: babel.config.js ================================================ // the babel config is for jest to parse jsx in test/spec files const presets = [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ]; module.exports = { presets }; ================================================ FILE: components/Alien.ts ================================================ import p5 from "p5"; import { K8sApi } from "@k8slens/extensions"; import { AlienImages } from "./Invaders"; class Alien { x: number y: number alienImages: AlienImages p5: p5 pod: K8sApi.Pod; hits: number; width: number; height: number; constructor(x: number, y: number, alienImages: AlienImages, p5: p5, pod: K8sApi.Pod) { this.x = x; this.y = y; this.alienImages = alienImages; this.p5 = p5; this.pod = pod this.hits = 0; this.width = alienImages.greenAlien.width / 20; this.height = alienImages.greenAlien.height / 20; } bulletHit(): number { this.pod.delete(); return this.hits += 1; } draw(): void { let image: p5.Image; this.p5.textSize(8); const status = this.pod.getStatus(); if (this.pod.metadata.deletionTimestamp || this.hits > 0) { image = this.alienImages.redAlien; } else if (status === "Running") { image = this.alienImages.greenAlien; } else if (status === "Pending") { image = this.alienImages.yellowAlien } else { image = this.alienImages.orangeAlien } image && this.p5.image(image, this.x, this.y, this.width, this.height); const name = this.pod.getName(); if (name && name.length > 7) { this.p5.text(`${name.substring(0, 7)}..`, this.x, this.y + 40); } else { this.p5.text(`${name}`, this.x, this.y + 40); } } } export default Alien ================================================ FILE: components/AlienBullet.ts ================================================ import Bullet from "./Bullet"; import p5 from "p5"; class AlienBullet extends Bullet { x: number; y: number; constructor(x: number, y: number, p5: p5) { super(x, y, p5); } update(): void { this.y += 2; } } export default AlienBullet ================================================ FILE: components/Bullet.ts ================================================ import p5 from "p5"; import Player from "./Player"; class Bullet { p5: p5; x: number; y: number; constructor(x: number, y: number, p5: p5) { this.x = x; this.y = y; this.p5 = p5; } isOffScreen(): boolean { return this.y <= 0; } draw(): void { this.p5.fill(255); this.p5.rect(this.x, this.y, 3, 15); } hasHit(player: Player): boolean { return this.p5.dist(this.x, this.y, player.x + 10, player.y + 10) < 20; } } export default Bullet; ================================================ FILE: components/ClusterPage.tsx ================================================ import React, { useEffect, useState } from "react" import Game from "./Game" import { K8sApi } from "@k8slens/extensions"; const ClusterPage = (): JSX.Element => { console.info("🔥 Cluster page rendered"); const [podsStore] = useState(K8sApi.apiManager.getStore(K8sApi.podsApi)) useEffect(() => { const ensure = async () => { if (!podsStore.isLoaded) { await podsStore.loadAll(); podsStore.subscribe(); } } ensure(); }, [podsStore]) return (
Space Invaders
) } export default ClusterPage ================================================ FILE: components/Game.tsx ================================================ import React, { memo, useState, useLayoutEffect } from "react" import { K8sApi } from "@k8slens/extensions"; import p5 from "p5"; import Invaders from "./Invaders"; import Player from "./Player"; import { IObservableArray } from "mobx"; import Particle from "./Particle"; import { configMapsStore } from "@k8slens/extensions/dist/src/renderer/components/+config-maps/config-maps.store"; type Props = { pods: IObservableArray } let keyboardEvenListener: (ev: KeyboardEvent) => void let mouseEvenListener: (ev: Event) => void; let windowResizeEvenListener: (ev: Event) => void; // an array to add multiple particles const particles: Array = []; const sketch = (pods: IObservableArray) => (p: p5) => { let invaders: Invaders; let player: Player; let enableParticles = false; let lastMouseTarget: EventTarget; let started = false; const gameImage = p.loadImage("https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/11a10a01-ac23-4fea-ad5a-b51f53084159/d5eu5dw-11a48688-3762-4f92-ba46-ebf94abe51b1.png/v1/fill/w_900,h_389,strp/space_invaders_logo__us__by_ringostarr39_d5eu5dw-fullview.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOiIsImlzcyI6InVybjphcHA6Iiwib2JqIjpbW3siaGVpZ2h0IjoiPD0zODkiLCJwYXRoIjoiXC9mXC8xMWExMGEwMS1hYzIzLTRmZWEtYWQ1YS1iNTFmNTMwODQxNTlcL2Q1ZXU1ZHctMTFhNDg2ODgtMzc2Mi00ZjkyLWJhNDYtZWJmOTRhYmU1MWIxLnBuZyIsIndpZHRoIjoiPD05MDAifV1dLCJhdWQiOlsidXJuOnNlcnZpY2U6aW1hZ2Uub3BlcmF0aW9ucyJdfQ.0wP1LBCQjWzqUQ5czHR2NjUv9iaiB2lUp_y-d9FuX-8"); const setup = () => { keyboardEvenListener && document.removeEventListener("keydown", keyboardEvenListener); mouseEvenListener && document.removeEventListener("mousedown", mouseEvenListener); windowResizeEvenListener && document.removeEventListener("resize", windowResizeEvenListener); const playerImage = p.loadImage("https://i.imgur.com/cCmEvHN.png"); const greenAlien = p.loadImage("https://i.imgur.com/fqeDYa0.png"); const redAlien = p.loadImage("https://i.imgur.com/iHKEnRq.png"); const yellowAlien = p.loadImage("https://i.imgur.com/lVEg9GG.png"); const orangeAlien = p.loadImage("https://i.imgur.com/LRYWNG0.png"); const container = document.getElementById("p5_canvas_container"); const canvas = p.createCanvas(container.offsetWidth, container.clientHeight + 100); canvas.parent(container); canvas.style("position", "relative"); canvas.attribute("tabindex", "1"); p.frameRate(24); invaders = new Invaders({ greenAlien, yellowAlien, orangeAlien, redAlien }, p, pods); player = new Player(playerImage, p, invaders); const starter = (event: Event) => { lastMouseTarget = event.target; if (event.target === canvas.elt) { started = true; } } document.addEventListener("mousedown", starter, false); mouseEvenListener = starter; const bind = ({ code }: { code: string }) => { if (code == "Backquote") { // for easier screen record w/o mouse started = !started; return; } if (lastMouseTarget !== canvas.elt) { return; } switch (code) { case "ArrowRight": player.moveRight() break; case "ArrowLeft": player.moveLeft() break; case "Space": console.info("🔫🔫🔫 You humans!") player.shoot() break; case "KeyS": enableParticles = !enableParticles; console.info("enableParticles", enableParticles) break; default: break; } }; document.addEventListener("keydown", bind); keyboardEvenListener = bind; for (let i = 0; i < p.width / 20; i++) { particles.push(new Particle(p)); } } p.setup = () => { setup(); }; const draw = () => { p.background(0); if (started) { invaders.update(player); invaders.draw(); player.update(); player.draw(); } else { const imageWidth = p.width / 3; const imageHeight = p.width / 6; const x = p.width / 2; const y = p.width / 2; const textSize = p.width / 50; p.image(gameImage, x - imageWidth / 2, y - imageHeight - textSize * 5, imageWidth, imageHeight); p.textSize(textSize); p.fill("rgba(0,255,0,1)"); p.textFont("Courier New"); p.textAlign(p.CENTER) p.text("CLICK TO START", x, y - textSize * 3); } if (player.score > (invaders.aliens.length * player.level)) { invaders.aliens = []; invaders.speed += 0.4; player.level += 1; } if (player.lives == 0) { p.textFont("Courier New"); p.textSize(p.width / 20); p.fill("rgba(0,255,0,1)"); const x = p.width / 2; const y = p.width / 2.5; p.text("GAME OVER ", x, y); p.fill("rgba(255,255,255,1)"); p.textSize(p.width / 80); p.text("CMD/Ctrl + R to Restart", x - p.width / 80, y + p.width / 40); player.stop(); } if (enableParticles) { for (let i = 0; i < particles.length; i++) { particles[i].createParticle(); particles[i].moveParticle(); particles[i].joinParticles(particles.slice(i)); } } } p.draw = () => { draw(); }; const resizer = () => { const container = document.getElementById("p5_canvas_container"); p.resizeCanvas(container.clientWidth, container.clientHeight); draw(); } window.addEventListener("resize", resizer); windowResizeEvenListener = resizer; }; const Game = memo(({ pods }: Props): JSX.Element => { const [init, setInit] = useState(false); useLayoutEffect(() => { if (!init) { new p5(sketch(pods)); console.info("👾 P5 Canvas Injected"); setInit(true) } }, [init, pods]); return (
) }) export default Game ================================================ FILE: components/Invaders.ts ================================================ import Alien from "./Alien"; import p5 from "p5"; import AlienBullet from "./AlienBullet"; import Bullet from "./Bullet"; import Player from "./Player"; import { K8sApi } from "@k8slens/extensions"; import { IObservableArray } from "mobx"; type AlienImages = { greenAlien: p5.Image, yellowAlien: p5.Image, orangeAlien: p5.Image, redAlien: p5.Image } class Invaders { alienImages: AlienImages; direction: number; y: number; aliens: Array = []; bullets: Array; speed: number; timeSinceLastBullet: number; p5: p5; pods: Array; constructor(alienImages: AlienImages, p5: p5, pods: IObservableArray) { this.alienImages = alienImages; this.direction = 0; this.y = 40; this.p5 = p5; this.bullets = []; this.speed = 0.2; this.pods = pods; // to make sure the aliens dont spam this.timeSinceLastBullet = 0; } update(player: Player): void { this.updateAliens(); for (const alien of this.aliens) { if (this.direction == 0) { alien.x += this.speed; } else if (this.direction == 1) { alien.x -= this.speed; } } this.updateBullets(player); if (this.hasChangedDirection()) { this.moveAlienDown(); } if (this.timeSinceLastBullet >= 40) { const bottomAliens = this.getBottomAliens(); if (bottomAliens.length) { this.makeABottomAlienShoot(bottomAliens); } } this.timeSinceLastBullet++; } hasChangedDirection(): boolean { for (const alien of this.aliens) { if (alien.x >= this.p5.width - 40) { this.direction = 1; return true; } else if (alien.x <= 20) { this.direction = 0; return true; } } return false; } moveAlienDown(): void { for (const alien of this.aliens) { alien.y += 10; } } // to make sure only the bottom row will shoot getBottomAliens(): Array { const allXPositions = this.getAllXPositions(); const aliensAtTheBottom = []; for (const alienAtX of allXPositions) { let bestYPosition = 0; let lowestAlien; for (const alien of this.aliens) { if (alien.x == alienAtX) { if (alien.y > bestYPosition) { bestYPosition = alien.y; lowestAlien = alien; } } } aliensAtTheBottom.push(lowestAlien); } return aliensAtTheBottom; } nextLevel(): void { this.speed += 0.5; this.updateAliens(); } // get all the x positions for a single frame getAllXPositions(): Set { const allXPositions = new Set(); for (const alien of this.aliens) { allXPositions.add(alien.x); } return allXPositions } updateAliens(): void { const maxX = this.p5.width - 100; const minX = 100; const gapX = 50; const newPods: K8sApi.Pod[] = [] this.pods.forEach((pod: K8sApi.Pod) => { const alien = this.aliens.find((a) => a.pod.getId() === pod.getId()) if(!alien) { newPods.push(pod); } else { alien.pod = pod; } }) const podIds = this.pods.map((p) => p.getId()); this.aliens.forEach((alien, index) => { if (!podIds.includes(alien.pod.getId())) { this.aliens.splice(index, 1); } }) const empty = this.aliens.length === 0; newPods.sort(() => Math.random() - 0.5); if (empty) { newPods.forEach((pod) => { let y = 80; let x = minX; const lastAlien = this.aliens[this.aliens.length - 1]; if (lastAlien) { x = lastAlien.x + gapX; y = lastAlien.y; if (x >= maxX) { x = minX; y = lastAlien.y + gapX; } } if (this.aliens.length < 100) { this.aliens.push(new Alien(x, y, this.alienImages, this.p5, pod)) } }) } else { const pod = newPods[0] if (!pod) { return } const alien = this.aliens[Math.floor(Math.random() * this.aliens.length)]; const aliensOnSameRow = this.aliens.filter((a) => a.y === alien.y) if (!aliensOnSameRow.find((a) => a.x >= (alien.x - (gapX + 3)) && a.x < alien.x && (alien.x - gapX) > minX)) { if (alien.x - gapX > minX) { this.aliens.push(new Alien(alien.x - gapX, alien.y, this.alienImages, this.p5, pod)) } } } } checkCollision(x: number, y: number): boolean { for (let i = this.aliens.length - 1; i >= 0; i--) { const currentAlien = this.aliens[i]; // the numbers are hard-coded for the width of the image if (this.p5.dist(x, y, currentAlien.x + (currentAlien.width / 2), currentAlien.y + (currentAlien.height / 2)) < currentAlien.width / 2) { currentAlien.bulletHit(); return true; } } return false; } makeABottomAlienShoot(bottomAliens: Array): void { const shootingAlien = this.p5.random(bottomAliens.filter((a) => a.hits === 0)); const bullet = new AlienBullet(shootingAlien.x + 10, shootingAlien.y + 10, this.p5); this.bullets.push(bullet); this.timeSinceLastBullet = 0; } updateBullets(player: Player): void { for (let i = this.bullets.length - 1; i >= 0; i--) { this.bullets[i].y += 4; if (this.bullets[i].hasHit(player)) { player.bulletHit(); this.bullets.splice(i, 1); } } } draw(): void { for (const alien of this.aliens) { alien.draw(); } for (const bullet of this.bullets) { this.p5.rect(bullet.x, bullet.y, 3, 10); } } } export { AlienImages } export default Invaders ================================================ FILE: components/Particle.ts ================================================ import p5 from "p5"; // this class describes the properties of a single particle. class Particle { // setting the co-ordinates, radius and the // speed of a particle in both the co-ordinates axes. x: number; y: number; r: number; xSpeed: number; ySpeed: number; p5: p5; constructor(p5: p5) { this.p5 = p5; this.x = this.p5.random(0, this.p5.windowWidth); this.y = this.p5.random(0, this.p5.windowHeight); this.r = this.p5.random(1, 8); this.xSpeed = this.p5.random(-2, 2); this.ySpeed = this.p5.random(-1, 1.5); } // creation of a particle. createParticle(): void { this.p5.noStroke(); this.p5.fill("rgba(200,169,169,0.5)"); this.p5.circle(this.x, this.y, this.r); } // setting the particle in motion. moveParticle(): void { if (this.x < 0 || this.x > this.p5.windowWidth) this.xSpeed *= -1; if (this.y < 0 || this.y > this.p5.windowHeight) this.ySpeed *= -1; this.x += this.xSpeed; this.y += this.ySpeed; } // this function creates the connections(lines) // between particles which are less than a certain distance apart joinParticles(particles: Array): void { particles.forEach((element: Particle) => { const dis = this.p5.dist(this.x, this.y, element.x, element.y); if (dis < 85) { this.p5.stroke("rgba(255,255,255,0.08)"); this.p5.line(this.x, this.y, element.x, element.y); } }); } } export default Particle; ================================================ FILE: components/Player.ts ================================================ import p5 from "p5"; import PlayerBullet from "./PlayerBullet" import Invaders from "./Invaders"; class Player { image: p5.Image; x: number; y: number; isMovingLeft: boolean; isMovingRight: boolean; bullets: Array; p5: p5; invaders: Invaders; lives: number; level: number; score: number; constructor(shooterImage: p5.Image, p5: p5, invaders: Invaders) { this.image = shooterImage; this.isMovingLeft = false; this.isMovingRight = false; this.bullets = []; this.p5 = p5; this.x = this.p5.width / 2; // there seems to be a bug of p5.height which won't update when window is being // resize, so we use windowHeight to cal y instead. this.y = this.p5.windowHeight - Math.max(210, this.p5.windowHeight / 3.8); this.invaders = invaders; this.lives = 3; this.score = 0; this.level = 1; } update(): void { if (this.isMovingRight) { this.x += 5; } else if (this.isMovingLeft) { this.x -= 5; } else { this.x = this.p5.width / 2; } this.y = this.p5.windowHeight - Math.max(210, this.p5.windowHeight / 3.8); if(this.invaders.aliens.find((a) => a.y >= this.p5.height)) { this.lives = 0; } this.constrain(); this.updateBullets(); } addScore(): void { this.score = this.score + 1; } updateBullets(): void { for (let i = this.bullets.length - 1; i >= 0; i--) { this.bullets[i].update(); if (this.hasHitAlien(this.bullets[i])) { this.bullets.splice(i, 1); this.addScore(); break; } else if (this.bullets[i].isOffScreen()) { this.bullets.splice(i, 1); break; } } } hasHitAlien(bullet: PlayerBullet): boolean { return this.invaders.checkCollision(bullet.x, bullet.y); } bulletHit(): void { if (this.lives > 0) { this.lives--; } } constrain(): void { if (this.x <= 0) { this.x = 0; } else if (this.x > this.p5.width - 23) { this.x = this.p5.width - 23; } } draw(): void { this.p5.image(this.image, this.x, this.y, this.image.width / 10, this.image.height / 10); this.drawBullets(); this.drawLives(); this.drawLevel(); this.drawScore(); } drawBullets(): void { for (const bullet of this.bullets) { bullet.draw(); } } drawLives(): void { this.p5.fill(255); this.p5.textSize(15); this.p5.text("LIVES", 50, 25); for (let i = 0; i < this.lives; i++) { this.p5.image(this.image, 100 + i * 30, 10, this.image.width / 20, this.image.height / 20); } } drawLevel(): void { this.p5.text("LEVEL", this.p5.width - 200, 25); this.p5.push(); this.p5.fill(100, 255, 100); this.p5.text(this.level, this.p5.width - 150, 25); this.p5.pop(); } drawScore(): void { this.p5.text("SCORE", this.p5.width - 120, 25); this.p5.push(); this.p5.fill(100, 255, 100); this.p5.text(this.score, this.p5.width - 50, 25); this.p5.pop(); } moveLeft(): void { this.isMovingRight = false; this.isMovingLeft = true; } moveRight(): void { this.isMovingLeft = false; this.isMovingRight = true; } stop(): void { this.isMovingLeft = false; this.isMovingRight = false; } shoot(): void { this.bullets.push(new PlayerBullet(this.x + 12, this.y, this.p5)); } } export default Player; ================================================ FILE: components/PlayerBullet.ts ================================================ import Bullet from "./Bullet" import p5 from "p5"; class PlayerBullet extends Bullet { constructor(x: number, y: number, p5: p5) { super(x, y, p5); } update(): void { this.y -= 12; } } export default PlayerBullet; ================================================ FILE: main.ts ================================================ import { LensMainExtension } from "@k8slens/extensions"; export default class MainExtension extends LensMainExtension { onActivate(): void { console.log("activated"); } onDeactivate(): void { console.log("deactivated"); } appMenus = [ { parentId: "help", label: "Invade", click(): void { this.navigate(); } } ] } ================================================ FILE: package.json ================================================ { "name": "@lensapp/lens-ext-invaders", "version": "0.0.7", "description": "Space Invaders for Lens", "publisher": "IRATA Inc", "main": "dist/main.js", "renderer": "dist/renderer.js", "engines": { "node": ">=12.0 <13.0" }, "contributes": { "crds": [], "cloudProviders": [], "kubernetesDistros": [] }, "keywords": [ "lens", "extension", "k8slens", "kubernetes" ], "scripts": { "start": "webpack --watch", "build": "npm run clean && webpack", "clean": "rm -rf ./dist", "test": "jest" }, "dependencies": { "@types/p5": "^0.9.1", "p5": "^1.1.9" }, "devDependencies": { "@babel/preset-env": "^7.12.7", "@babel/preset-react": "^7.12.7", "@babel/preset-typescript": "^7.12.7", "@jest-runner/electron": "^3.0.0", "@k8slens/extensions": "4.2.1", "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", "@types/jest": "^26.0.15", "@types/node": "^12.12.9", "@types/react": "^17.0.0", "@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/parser": "^4.8.1", "electron": "^11.0.3", "eslint": "^7.13.0", "eslint-plugin-react-hooks": "^4.2.0", "jest": "^26.6.3", "react": "^17.0.1", "react-dom": "^17.0.1", "mobx": "^5.0.0", "mobx-react": "^6.2.2", "ts-loader": "^8.0.11", "typescript": "^4.1.2", "webpack": "^4.44.2", "webpack-cli": "^3.3.11" }, "jest": { "runner": "@jest-runner/electron", "testEnvironment": "@jest-runner/electron/environment" }, "lingui": { "locales": [ "en" ] } } ================================================ FILE: renderer.tsx ================================================ import { LensRendererExtension, Component } from "@k8slens/extensions"; import React from "react" import ClusterPage from "./components/ClusterPage"; const { Icon } = Component; export default class RendererExtension extends LensRendererExtension { #clusterPageId = "space_invader_clusters_page"; clusterPages = [ { id: this.#clusterPageId, title: "Space Invaders", components: { Page: ClusterPage } }, ] clusterPageMenus = [ // a cluster menu item which links to a cluster page { title: "Space Invaders", target: { pageId: this.#clusterPageId, params: {} }, components: { Icon: (): JSX.Element => , } }, ] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "outDir": "dist", "module": "CommonJS", "target": "ES2017", "lib": [ "ESNext", "DOM", "DOM.Iterable" ], "moduleResolution": "Node", "sourceMap": false, "declaration": false, "strict": false, "noImplicitAny": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "jsx": "react" }, "include": [ "./*.ts", "./*.tsx" ], "exclude": [ "node_modules", "*.js" ] } ================================================ FILE: webpack.config.js ================================================ /* eslint @typescript-eslint/no-var-requires: "off" */ const path = require("path"); module.exports = [ { entry: "./main.ts", context: __dirname, target: "electron-main", mode: "development", devtool: "eval-source-map", module: { rules: [ { test: /\.tsx?$/, use: [ { loader: "ts-loader", options: { transpileOnly: true, } } ], exclude: /node_modules/, }, ], }, externals: [ { "@k8slens/extensions": "var global.LensExtensions", "mobx": "var global.Mobx", "react": "var global.React", "mobx-react": "var global.MobxReact" } ], optimization: { minimize: false, }, resolve: { extensions: [".tsx", ".ts", ".js"], }, output: { libraryTarget: "commonjs2", filename: "main.js", path: path.resolve(__dirname, "dist"), }, }, { entry: "./renderer.tsx", context: __dirname, target: "electron-renderer", mode: "development", devtool: "eval-source-map", module: { rules: [ { test: /\.tsx?$/, use: [ { loader: "ts-loader", options: { transpileOnly: true, } } ], exclude: /node_modules/, }, ], }, optimization: { minimize: false, }, externals: [ { "@k8slens/extensions": "var global.LensExtensions", "react": "var global.React", "mobx": "var global.Mobx" } ], resolve: { extensions: [".tsx", ".ts", ".js"], }, output: { libraryTarget: "commonjs2", globalObject: "this", filename: "renderer.js", path: path.resolve(__dirname, "dist"), }, node: { __dirname: false, __filename: false } }, ];