[
  {
    "path": ".github/workflows/workflow.yml",
    "content": "name: workflow\n\non:\n  push:\n    tags:\n      - '*.*.*'\n    branches:\n      - '**'\n  pull_request:\n    branches:\n      - master\n\npermissions:\n  id-token: write\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    env:\n      TZ: Europe/Amsterdam\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          registry-url: https://registry.npmjs.org/\n      - run: |\n          npm ci --ignore-scripts --legacy-peer-deps\n          npm run build\n\n  publish:\n    if: startsWith(github.ref, 'refs/tags/')\n    needs: [build]\n    runs-on: ubuntu-latest\n    env:\n      TZ: Europe/Amsterdam\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          registry-url: https://registry.npmjs.org/ \n      - run: |\n          version=${{ github.ref_name }}\n          sed -i \"s/{{PLACEHOLDER_VERSION}}/${version}/\" ./src/version.ts\n          cat ./src/version.ts\n      - run: |\n          npm ci --ignore-scripts --legacy-peer-deps\n          npm run build\n      - run: |\n          cp README.md ./dist\n          cp LICENSE ./dist\n          cp package.json ./dist\n          cd dist && npm publish\n"
  },
  {
    "path": ".gitignore",
    "content": "# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore\n\n# Logs\n\nlogs\n_.log\nnpm-debug.log_\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Caches\n\n.cache\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\n\nreport.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json\n\n# Runtime data\n\npids\n_.pid\n_.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\n\nlib-cov\n\n# Coverage directory used by tools like istanbul\n\ncoverage\n*.lcov\n\n# nyc test coverage\n\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n\n.grunt\n\n# Bower dependency directory (https://bower.io/)\n\nbower_components\n\n# node-waf configuration\n\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\n\nbuild/Release\n\n# Dependency directories\n\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\n\nweb_modules/\n\n# TypeScript cache\n\n*.tsbuildinfo\n\n# Optional npm cache directory\n\n.npm\n\n# Optional eslint cache\n\n.eslintcache\n\n# Optional stylelint cache\n\n.stylelintcache\n\n# Microbundle cache\n\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n\n.node_repl_history\n\n# Output of 'npm pack'\n\n*.tgz\n\n# Yarn Integrity file\n\n.yarn-integrity\n\n# dotenv environment variable files\n\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n\n.parcel-cache\n\n# Next.js build output\n\n.next\nout\n\n# Nuxt.js build / generate output\n\n.nuxt\ndist\n\n# Gatsby files\n\n# Comment in the public line in if your project uses Gatsby and not Next.js\n\n# https://nextjs.org/blog/next-9-1#public-directory-support\n\n# public\n\n# vuepress build output\n\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n\n.temp\n\n# Docusaurus cache and generated files\n\n.docusaurus\n\n# Serverless directories\n\n.serverless/\n\n# FuseBox cache\n\n.fusebox/\n\n# DynamoDB Local files\n\n.dynamodb/\n\n# TernJS port file\n\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# IntelliJ based IDEs\n.idea\n\n# Finder (MacOS) folder config\n.DS_Store\n\nnode_modules/\nlib/\ntest/\ndist/\n"
  },
  {
    "path": ".npmrc",
    "content": "tag-version-prefix=\"\""
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Lars Kniep\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# @larscom/ng-chrome-extension\n\n[![npm-release](https://img.shields.io/npm/v/@larscom/ng-chrome-extension.svg?label=npm)](https://www.npmjs.com/package/@larscom/ng-chrome-extension)\n![npm](https://img.shields.io/npm/dt/@larscom/ng-chrome-extension)\n\n> Easily create `Angular` Chrome Extensions (manifest v3)\n\nThe following scenarios are supported:\n\n- Popup &#10003;\n- New Tab &#10003;\n- Options &#10003;\n- Side Panel &#10003;\n- Service Worker &#10003;\n- Content Page &#10003;\n\n## Disclaimer\n\nThis CLI tool should work on Linux/macOS, it is not tested on Windows.\n\n## How to install\n\n```bash\nnpm install -g @larscom/ng-chrome-extension\n```\n\n## Start creating a new project\n\n```bash\nng-chrome new\n```\n\n![alt text](https://snipboard.io/OYcNzx.jpg 'ng-chrome CLI')\n\n## How to use/develop\n\n- change directory to your newly created project\n- run `npm run start`\n- goto: `chrome://extensions` in the browser and enable `'developer mode'`\n- press `Load unpacked` and target the folder `angular/dist`\n\nThe project is automatically being watched, any changes to the files will recompile the project.\n\n**NOTE**: changes to the **content page** and **service worker** scripts requires you to reload the extension in `chrome://extensions`\n\n![alt text](https://snipboard.io/KToCI3.jpg 'Angular Chrome Popup')\n![alt text](https://snipboard.io/VYfGoD.jpg 'Angular Chrome Tab')\n\n## Build/package for production\n\n- update version number inside `./angular/src/manifest.json`\n- run `npm run build:production`\n- upload `extension-build.zip` to the chrome webstore.\n\nThis will run a production build and will automatically zip it as a extension package in the root folder named: `extension-build.zip`\n\n## Debugging\n\nRun: `npm start`\n\nGo to: Developer tools (inspect popup) => Sources => webpack\n\nYou can find your source files (TypeScript) over there.\n\n## Upgrade Angular\n\nAfter you have created a new project with `ng-chrome` and you want to update angular.\n\nJust follow the regular upgrade guide of angular. See: https://update.angular.io/\n\n## Angular folder\n\nThis folder contains the angular source code.\nEach feature (popup,options,tab,side-panel) lives inside its own standalone component and gets lazily loaded.\n\nsee: `./angular/src/app/modules`\n\n## Chrome folder\n\nThis folder contains the content page/service worker scripts and has its own `package.json` to manage it's dependencies.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@larscom/ng-chrome-extension\",\n  \"shortName\": \"ng-chrome\",\n  \"version\": \"3.2.4\",\n  \"description\": \"CLI to generate angular chrome extensions\",\n  \"main\": \"./bin/main.js\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"ng-chrome\": \"bin/main.js\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/larscom/ng-chrome-extension.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/larscom/ng-chrome-extension/issues\"\n  },\n  \"homepage\": \"https://github.com/larscom/ng-chrome-extension#readme\",\n  \"scripts\": {\n    \"start\": \"rollup -c && node ./dist/bin/main.js\",\n    \"build\": \"rollup -c\"\n  },\n  \"keywords\": [\n    \"ng-chrome\",\n    \"ng\",\n    \"angular\",\n    \"chrome\",\n    \"extension\",\n    \"google\",\n    \"manifest\",\n    \"rxjs\"\n  ],\n  \"author\": \"Lars Kniep\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"adm-zip\": \"^0.5.14\",\n    \"axios\": \"^1.7.2\",\n    \"clear\": \"^0.1.0\",\n    \"commander\": \"^12.1.0\",\n    \"figlet\": \"^1.7.0\",\n    \"fs-extra\": \"^11.2.0\",\n    \"inquirer\": \"^12.9.4\",\n    \"kleur\": \"^4.1.5\",\n    \"ora\": \"^8.0.1\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^29.0.0\",\n    \"@rollup/plugin-json\": \"^6.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"@rollup/plugin-typescript\": \"^11.1.6\",\n    \"@types/adm-zip\": \"^0.5.5\",\n    \"@types/bun\": \"latest\",\n    \"@types/clear\": \"^0.1.4\",\n    \"@types/figlet\": \"^1.5.8\",\n    \"@types/fs-extra\": \"^11.0.4\",\n    \"@types/inquirer\": \"^9.0.7\",\n    \"@types/node\": \"^20.14.8\",\n    \"rimraf\": \"^6.1.0\",\n    \"rollup\": \"^4.18.0\",\n    \"typescript\": \"^5.5.2\"\n  }\n}\n"
  },
  {
    "path": "rollup.config.js",
    "content": "import commonjs from '@rollup/plugin-commonjs'\nimport json from '@rollup/plugin-json'\nimport resolve from '@rollup/plugin-node-resolve'\nimport typescript from '@rollup/plugin-typescript'\n\nexport default {\n  input: './src/main.ts',\n  output: {\n    file: './dist/bin/main.js'\n  },\n  external: ['adm-zip', 'axios', 'clear', 'commander', 'figlet', 'fs-extra', 'inquirer', 'kleur', 'ora'],\n  plugins: [typescript(), resolve(), commonjs(), json()]\n}\n"
  },
  {
    "path": "src/logger.ts",
    "content": "import clear from 'clear'\nimport figlet from 'figlet'\nimport kleur from 'kleur'\n\nconst red = kleur.red\nconst yellow = kleur.yellow\nconst cyan = kleur.cyan\nconst green = kleur.green\nconst bold = kleur.bold\n\nexport class Logger {\n  private readonly githubUrl: string\n\n  constructor(githubUrl: string) {\n    this.githubUrl = githubUrl\n  }\n\n  error(message: string, ...optionalParams: any[]) {\n    console.log(red(`ERROR: ${message}`), ...optionalParams)\n  }\n\n  warn(message: string, ...optionalParams: any[]) {\n    console.log(yellow(`WARN: ${message}`), ...optionalParams)\n  }\n\n  info(message: string, ...optionalParams: any[]) {\n    console.log(cyan(`INFO: ${message}`), ...optionalParams)\n  }\n\n  showIntro(name: string, version: string) {\n    clear({ fullClear: true })\n\n    console.log(red(figlet.textSync(name, { horizontalLayout: 'full' })))\n    console.log(green(version))\n    console.log('---------------------------------------------------------------')\n    console.log(`${this.githubUrl}`)\n    console.log()\n  }\n\n  showOutro(name: string) {\n    console.log()\n    console.log('---------------------------------------------------------------')\n    console.log(`You can now change directory to ${yellow(name)} and type the following commands:`)\n\n    console.log()\n    console.log(`   ${cyan('npm start')}`)\n    console.log('       The project is automatically being watched/build')\n    console.log(`       Go to ${yellow('chrome://extensions')} in the browser and enable '${bold('developer mode')}'`)\n    console.log(`       Press ${yellow('Load unpacked')} and target the folder '${bold('angular/dist')}'`)\n\n    console.log()\n    console.log(`   ${cyan('npm run build:production')}`)\n    console.log('       Creates a production ready zip file')\n    console.log(`       Upload ${yellow('extension-build.zip')} directly to the chrome webstore`)\n    console.log()\n  }\n}\n"
  },
  {
    "path": "src/main.ts",
    "content": "#!/usr/bin/env node\n\nimport admZip from 'adm-zip'\nimport axios from 'axios'\nimport { exec } from 'child_process'\nimport { program } from 'commander'\nimport fs from 'fs-extra'\nimport inquirer from 'inquirer'\nimport path from 'path'\nimport { Logger } from './logger'\nimport { Spinner } from './spinner'\nimport { version } from './version'\n\nconst packageName = 'ng-chrome'\nconst githubUrl = 'https://github.com/larscom/ng-chrome-extension'\nconst templateUrl = 'https://github.com/larscom/angular-chrome-extension/archive/refs/heads/master.zip'\n\nconst nameRegex = new RegExp(/^[a-z0-9-_]+$/)\nconst log = new Logger(githubUrl)\nconst spinner = new Spinner()\n\nprogram\n  .name(packageName)\n  .description(`Create Google Chrome (V3) extensions with Angular!\\n${githubUrl}`)\n  .version(version)\n\n  .command('new [name]')\n  .description('Create a new Angular Chrome extension')\n  .action(handleNewProject)\n\nprogram.parse(process.argv, { from: 'node' })\n\nasync function handleNewProject(name: string): Promise<void> {\n  log.showIntro(packageName, version)\n  if (name) {\n    const projectName = await parseName(name)\n    await setupNewProject(projectName)\n  } else {\n    const { name } = await askName('name')\n    const projectName = await parseName(name)\n    await setupNewProject(projectName)\n  }\n}\n\nasync function setupNewProject(name: string): Promise<void> {\n  await createProject(name)\n  await installDeps(name)\n\n  log.showOutro(name)\n}\n\nasync function createProject(name: string): Promise<void> {\n  const projectDir = getProjectDir(name)\n\n  try {\n    spinner.start('Creating new extension...')\n\n    await downloadTemplate(projectDir, templateUrl)\n\n    spinner.stop(`Created new Angular chrome extension at: ${projectDir}`)\n  } catch (e) {\n    spinner.stop()\n    log.error('Failed creating new extension', e)\n    process.exit(1)\n  }\n}\n\nasync function installDeps(name: string): Promise<void> {\n  process.chdir(getProjectDir(name))\n\n  try {\n    spinner.start('Installing dependencies...')\n\n    await execCmd('npm ci --legacy-peer-deps')\n    await execCmd('(cd chrome && npm ci --legacy-peer-deps)')\n\n    spinner.stop('Successfully installed dependencies')\n  } catch (e) {\n    spinner.stop()\n    log.error('Failed installing dependencies', e)\n    process.exit(1)\n  }\n}\n\nasync function downloadTemplate(dir: string, templateUrl: string): Promise<void> {\n  await fs.mkdirp(dir)\n  const zip = new admZip(await getZipBuffer(templateUrl))\n\n  const entries = zip.getEntries()\n  for (const entry of entries) {\n    const path = `${dir}/${entry.entryName.replace('angular-chrome-extension-master/', '')}`\n    if (path.includes('..')) {\n      const msg = `Unexpected path: ${path}`\n      log.error(msg)\n      throw Error(msg)\n    }\n\n    if (entry.isDirectory) {\n      fs.mkdirpSync(path)\n    } else {\n      fs.writeFileSync(path, entry.getData().toString('utf-8'))\n    }\n  }\n}\n\nasync function parseName(name: string): Promise<string> {\n  if (isValidName(name)) {\n    const exist = await dirExists(name)\n    if (exist) {\n      log.error(`Project with name '${name}' already exists`)\n      process.exit(1)\n    }\n    return name\n  } else {\n    log.error(`Project name invalid, must match: ${nameRegex.toString()}`)\n    process.exit(1)\n  }\n}\n\nasync function getZipBuffer(url: string) {\n  return await axios({ url, responseType: 'arraybuffer' }).then(({ data }) => data)\n}\n\nasync function execCmd(command: string): Promise<void> {\n  return new Promise<void>((resolve, reject) => exec(command, (error) => (error ? reject(error) : resolve())))\n}\n\nfunction dirExists(name: string): Promise<boolean> {\n  return fs.pathExists(getProjectDir(name))\n}\n\nfunction getProjectDir(name: string): string {\n  return path.join(process.cwd(), name)\n}\n\nfunction isValidName(name: string): boolean {\n  return nameRegex.test(name)\n}\n\nfunction askName(name: string): Promise<any> {\n  return inquirer.prompt([{ name, type: 'input', message: 'Enter a project name:' }])\n}\n"
  },
  {
    "path": "src/spinner.ts",
    "content": "import ora from 'ora'\n\nexport class Spinner {\n  private loader = ora({ color: 'green', hideCursor: true })\n\n  start(text: string) {\n    this.loader.start(text)\n  }\n\n  stop(text?: string) {\n    this.loader.succeed(text)\n  }\n}\n"
  },
  {
    "path": "src/version.ts",
    "content": "export const version = '{{PLACEHOLDER_VERSION}}'\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"module\": \"ESNext\",\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"moduleResolution\": \"Node\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  }
]