Repository: larscom/ng-chrome-extension Branch: master Commit: 709a1dc3283c Files: 12 Total size: 15.0 KB Directory structure: gitextract_1ulcxw8j/ ├── .github/ │ └── workflows/ │ └── workflow.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src/ │ ├── logger.ts │ ├── main.ts │ ├── spinner.ts │ └── version.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/workflow.yml ================================================ name: workflow on: push: tags: - '*.*.*' branches: - '**' pull_request: branches: - master permissions: id-token: write contents: read jobs: build: runs-on: ubuntu-latest env: TZ: Europe/Amsterdam steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 registry-url: https://registry.npmjs.org/ - run: | npm ci --ignore-scripts --legacy-peer-deps npm run build publish: if: startsWith(github.ref, 'refs/tags/') needs: [build] runs-on: ubuntu-latest env: TZ: Europe/Amsterdam steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 registry-url: https://registry.npmjs.org/ - run: | version=${{ github.ref_name }} sed -i "s/{{PLACEHOLDER_VERSION}}/${version}/" ./src/version.ts cat ./src/version.ts - run: | npm ci --ignore-scripts --legacy-peer-deps npm run build - run: | cp README.md ./dist cp LICENSE ./dist cp package.json ./dist cd dist && npm publish ================================================ FILE: .gitignore ================================================ # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Caches .cache # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store node_modules/ lib/ test/ dist/ ================================================ FILE: .npmrc ================================================ tag-version-prefix="" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Lars Kniep 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 ================================================ # @larscom/ng-chrome-extension [![npm-release](https://img.shields.io/npm/v/@larscom/ng-chrome-extension.svg?label=npm)](https://www.npmjs.com/package/@larscom/ng-chrome-extension) ![npm](https://img.shields.io/npm/dt/@larscom/ng-chrome-extension) > Easily create `Angular` Chrome Extensions (manifest v3) The following scenarios are supported: - Popup ✓ - New Tab ✓ - Options ✓ - Side Panel ✓ - Service Worker ✓ - Content Page ✓ ## Disclaimer This CLI tool should work on Linux/macOS, it is not tested on Windows. ## How to install ```bash npm install -g @larscom/ng-chrome-extension ``` ## Start creating a new project ```bash ng-chrome new ``` ![alt text](https://snipboard.io/OYcNzx.jpg 'ng-chrome CLI') ## How to use/develop - change directory to your newly created project - run `npm run start` - goto: `chrome://extensions` in the browser and enable `'developer mode'` - press `Load unpacked` and target the folder `angular/dist` The project is automatically being watched, any changes to the files will recompile the project. **NOTE**: changes to the **content page** and **service worker** scripts requires you to reload the extension in `chrome://extensions` ![alt text](https://snipboard.io/KToCI3.jpg 'Angular Chrome Popup') ![alt text](https://snipboard.io/VYfGoD.jpg 'Angular Chrome Tab') ## Build/package for production - update version number inside `./angular/src/manifest.json` - run `npm run build:production` - upload `extension-build.zip` to the chrome webstore. This will run a production build and will automatically zip it as a extension package in the root folder named: `extension-build.zip` ## Debugging Run: `npm start` Go to: Developer tools (inspect popup) => Sources => webpack You can find your source files (TypeScript) over there. ## Upgrade Angular After you have created a new project with `ng-chrome` and you want to update angular. Just follow the regular upgrade guide of angular. See: https://update.angular.io/ ## Angular folder This folder contains the angular source code. Each feature (popup,options,tab,side-panel) lives inside its own standalone component and gets lazily loaded. see: `./angular/src/app/modules` ## Chrome folder This folder contains the content page/service worker scripts and has its own `package.json` to manage it's dependencies. ================================================ FILE: package.json ================================================ { "name": "@larscom/ng-chrome-extension", "shortName": "ng-chrome", "version": "3.2.4", "description": "CLI to generate angular chrome extensions", "main": "./bin/main.js", "type": "module", "bin": { "ng-chrome": "bin/main.js" }, "publishConfig": { "access": "public" }, "repository": { "type": "git", "url": "git+https://github.com/larscom/ng-chrome-extension.git" }, "bugs": { "url": "https://github.com/larscom/ng-chrome-extension/issues" }, "homepage": "https://github.com/larscom/ng-chrome-extension#readme", "scripts": { "start": "rollup -c && node ./dist/bin/main.js", "build": "rollup -c" }, "keywords": [ "ng-chrome", "ng", "angular", "chrome", "extension", "google", "manifest", "rxjs" ], "author": "Lars Kniep", "license": "MIT", "dependencies": { "adm-zip": "^0.5.14", "axios": "^1.7.2", "clear": "^0.1.0", "commander": "^12.1.0", "figlet": "^1.7.0", "fs-extra": "^11.2.0", "inquirer": "^12.9.4", "kleur": "^4.1.5", "ora": "^8.0.1" }, "devDependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^11.1.6", "@types/adm-zip": "^0.5.5", "@types/bun": "latest", "@types/clear": "^0.1.4", "@types/figlet": "^1.5.8", "@types/fs-extra": "^11.0.4", "@types/inquirer": "^9.0.7", "@types/node": "^20.14.8", "rimraf": "^6.1.0", "rollup": "^4.18.0", "typescript": "^5.5.2" } } ================================================ FILE: rollup.config.js ================================================ import commonjs from '@rollup/plugin-commonjs' import json from '@rollup/plugin-json' import resolve from '@rollup/plugin-node-resolve' import typescript from '@rollup/plugin-typescript' export default { input: './src/main.ts', output: { file: './dist/bin/main.js' }, external: ['adm-zip', 'axios', 'clear', 'commander', 'figlet', 'fs-extra', 'inquirer', 'kleur', 'ora'], plugins: [typescript(), resolve(), commonjs(), json()] } ================================================ FILE: src/logger.ts ================================================ import clear from 'clear' import figlet from 'figlet' import kleur from 'kleur' const red = kleur.red const yellow = kleur.yellow const cyan = kleur.cyan const green = kleur.green const bold = kleur.bold export class Logger { private readonly githubUrl: string constructor(githubUrl: string) { this.githubUrl = githubUrl } error(message: string, ...optionalParams: any[]) { console.log(red(`ERROR: ${message}`), ...optionalParams) } warn(message: string, ...optionalParams: any[]) { console.log(yellow(`WARN: ${message}`), ...optionalParams) } info(message: string, ...optionalParams: any[]) { console.log(cyan(`INFO: ${message}`), ...optionalParams) } showIntro(name: string, version: string) { clear({ fullClear: true }) console.log(red(figlet.textSync(name, { horizontalLayout: 'full' }))) console.log(green(version)) console.log('---------------------------------------------------------------') console.log(`${this.githubUrl}`) console.log() } showOutro(name: string) { console.log() console.log('---------------------------------------------------------------') console.log(`You can now change directory to ${yellow(name)} and type the following commands:`) console.log() console.log(` ${cyan('npm start')}`) console.log(' The project is automatically being watched/build') console.log(` Go to ${yellow('chrome://extensions')} in the browser and enable '${bold('developer mode')}'`) console.log(` Press ${yellow('Load unpacked')} and target the folder '${bold('angular/dist')}'`) console.log() console.log(` ${cyan('npm run build:production')}`) console.log(' Creates a production ready zip file') console.log(` Upload ${yellow('extension-build.zip')} directly to the chrome webstore`) console.log() } } ================================================ FILE: src/main.ts ================================================ #!/usr/bin/env node import admZip from 'adm-zip' import axios from 'axios' import { exec } from 'child_process' import { program } from 'commander' import fs from 'fs-extra' import inquirer from 'inquirer' import path from 'path' import { Logger } from './logger' import { Spinner } from './spinner' import { version } from './version' const packageName = 'ng-chrome' const githubUrl = 'https://github.com/larscom/ng-chrome-extension' const templateUrl = 'https://github.com/larscom/angular-chrome-extension/archive/refs/heads/master.zip' const nameRegex = new RegExp(/^[a-z0-9-_]+$/) const log = new Logger(githubUrl) const spinner = new Spinner() program .name(packageName) .description(`Create Google Chrome (V3) extensions with Angular!\n${githubUrl}`) .version(version) .command('new [name]') .description('Create a new Angular Chrome extension') .action(handleNewProject) program.parse(process.argv, { from: 'node' }) async function handleNewProject(name: string): Promise { log.showIntro(packageName, version) if (name) { const projectName = await parseName(name) await setupNewProject(projectName) } else { const { name } = await askName('name') const projectName = await parseName(name) await setupNewProject(projectName) } } async function setupNewProject(name: string): Promise { await createProject(name) await installDeps(name) log.showOutro(name) } async function createProject(name: string): Promise { const projectDir = getProjectDir(name) try { spinner.start('Creating new extension...') await downloadTemplate(projectDir, templateUrl) spinner.stop(`Created new Angular chrome extension at: ${projectDir}`) } catch (e) { spinner.stop() log.error('Failed creating new extension', e) process.exit(1) } } async function installDeps(name: string): Promise { process.chdir(getProjectDir(name)) try { spinner.start('Installing dependencies...') await execCmd('npm ci --legacy-peer-deps') await execCmd('(cd chrome && npm ci --legacy-peer-deps)') spinner.stop('Successfully installed dependencies') } catch (e) { spinner.stop() log.error('Failed installing dependencies', e) process.exit(1) } } async function downloadTemplate(dir: string, templateUrl: string): Promise { await fs.mkdirp(dir) const zip = new admZip(await getZipBuffer(templateUrl)) const entries = zip.getEntries() for (const entry of entries) { const path = `${dir}/${entry.entryName.replace('angular-chrome-extension-master/', '')}` if (path.includes('..')) { const msg = `Unexpected path: ${path}` log.error(msg) throw Error(msg) } if (entry.isDirectory) { fs.mkdirpSync(path) } else { fs.writeFileSync(path, entry.getData().toString('utf-8')) } } } async function parseName(name: string): Promise { if (isValidName(name)) { const exist = await dirExists(name) if (exist) { log.error(`Project with name '${name}' already exists`) process.exit(1) } return name } else { log.error(`Project name invalid, must match: ${nameRegex.toString()}`) process.exit(1) } } async function getZipBuffer(url: string) { return await axios({ url, responseType: 'arraybuffer' }).then(({ data }) => data) } async function execCmd(command: string): Promise { return new Promise((resolve, reject) => exec(command, (error) => (error ? reject(error) : resolve()))) } function dirExists(name: string): Promise { return fs.pathExists(getProjectDir(name)) } function getProjectDir(name: string): string { return path.join(process.cwd(), name) } function isValidName(name: string): boolean { return nameRegex.test(name) } function askName(name: string): Promise { return inquirer.prompt([{ name, type: 'input', message: 'Enter a project name:' }]) } ================================================ FILE: src/spinner.ts ================================================ import ora from 'ora' export class Spinner { private loader = ora({ color: 'green', hideCursor: true }) start(text: string) { this.loader.start(text) } stop(text?: string) { this.loader.succeed(text) } } ================================================ FILE: src/version.ts ================================================ export const version = '{{PLACEHOLDER_VERSION}}' ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES6", "module": "ESNext", "outDir": "./dist", "rootDir": "./src", "moduleResolution": "Node", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src"], "exclude": ["node_modules", "dist"] }