Initial commit
This commit is contained in:
3
.example.env
Normal file
3
.example.env
Normal file
@@ -0,0 +1,3 @@
|
||||
CEB_EXAMPLE=example_env
|
||||
CEB_DEV_LOCALE=
|
||||
CEB_CI=
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
*.sh text eol=lf
|
||||
2
.gitguardian.yaml
Normal file
2
.gitguardian.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
paths_ignore:
|
||||
- "pnpm-lock.yaml"
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @Jonghakseo
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: Jonghakseo
|
||||
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report.
|
||||
title: "[Bug]: "
|
||||
labels: [ "bug", "triage" ]
|
||||
assignees:
|
||||
- Jonghakseo
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: to-reproduce
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: |
|
||||
Steps to reproduce the behavior.
|
||||
Use [Brie](https://go.briehq.com/github?utm_source=CEB) to capture screenshots, errors, and network activity for faster issue resolution.
|
||||
placeholder: Describe the steps to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
placeholder: Describe the expected behavior.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
|
||||
- type: dropdown
|
||||
id: desktop-os
|
||||
attributes:
|
||||
label: OS
|
||||
description: Select your operating system.
|
||||
options: [ Windows, macOS, Linux ]
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: desktop-browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Select the browser where the issue occurs(Multiple choice).
|
||||
multiple: true
|
||||
options: [ Chrome, Opera, Opera GX, Firefox, Safari, Microsoft Edge, Brave ]
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: desktop-node-version
|
||||
attributes:
|
||||
label: Node Version
|
||||
description: Specify the Node.js version you are using.
|
||||
placeholder: 22.12.0
|
||||
|
||||
- type: input
|
||||
id: desktop-other-packages
|
||||
attributes:
|
||||
label: Other Necessary Packages Version
|
||||
description: Specify versions of other necessary packages.
|
||||
placeholder: pnpm-10.10.0
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context about the problem here.
|
||||
placeholder: Provide additional details if necessary.
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: GitHub Community Support
|
||||
url: https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/discussions
|
||||
about: Please leave your questions here or on Discord - Link below "Security Bug Bounty"
|
||||
- name: GitHub Security Bug Bounty
|
||||
url: https://discord.gg/RhETcmdW
|
||||
about: Please report security vulnerabilities here(Send DM to Jonghakseo or tell us on chat, you want to report it).
|
||||
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for improvement.
|
||||
title: "[Feature Request]: "
|
||||
labels: [ "request-new-feature", "triage" ]
|
||||
assignees:
|
||||
- Jonghakseo
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: A clear and concise description of what's the problem. Ex. I'm looking for a way to...
|
||||
placeholder: Describe the problem or frustration.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: Describe the solution you'd like.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: A clear and concise description of any alternative solutions.
|
||||
placeholder: Describe alternative solutions or features.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
placeholder: Provide additional details if necessary.
|
||||
10
.github/auto_assign.yml
vendored
Normal file
10
.github/auto_assign.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
addReviewers: true
|
||||
|
||||
reviewers:
|
||||
- Jonghakseo
|
||||
|
||||
addAssignees: author
|
||||
|
||||
filterLabels:
|
||||
exclude:
|
||||
- dependencies
|
||||
25
.github/dependabot.yml
vendored
Normal file
25
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
target-branch: 'dev'
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "npm"
|
||||
groups:
|
||||
dev-dependencies:
|
||||
dependency-type: development
|
||||
dependencies:
|
||||
dependency-type: production
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
target-branch: 'dev'
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
25
.github/pull_request_template.md
vendored
Normal file
25
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<!-- Note: Please ensure your PR is targeting the `dev` branch -->
|
||||
|
||||
<!-- Describe what this PR is for in the title. -->
|
||||
|
||||
> `*` denotes required fields
|
||||
|
||||
## Priority*
|
||||
|
||||
- [ ] High: This PR needs to be merged first, before other tasks.
|
||||
- [x] Medium: This PR should be merged quickly to prevent conflicts due to common changes. (default)
|
||||
- [ ] Low: This PR does not affect other tasks, so it can be merged later.
|
||||
|
||||
## Purpose of the PR*
|
||||
<!-- Describe the purpose of the PR. -->
|
||||
|
||||
## Changes*
|
||||
|
||||
|
||||
## How to check the feature
|
||||
<!-- Describe how to check the feature in detail -->
|
||||
<!-- If there are any visual changes, please attach a screenshot for easy identification. -->
|
||||
|
||||
|
||||
## Reference
|
||||
<!-- Any helpful information for understanding the PR. -->
|
||||
2
.github/secret_scanning.yml
vendored
Normal file
2
.github/secret_scanning.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
paths-ignore:
|
||||
- "../pnpm-lock.yaml"
|
||||
21
.github/stale.yml
vendored
Normal file
21
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 90
|
||||
# Number of days of inactivity before a stale Issue or Pull Request is closed
|
||||
daysUntilClose: 30
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when removing the stale label. Set to `false` to disable
|
||||
unmarkComment: false
|
||||
# Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable
|
||||
closeComment: true
|
||||
# Limit to only `issues` or `pulls`
|
||||
only: issues
|
||||
24
.github/workflows/auto-change-prs-branch.yml
vendored
Normal file
24
.github/workflows/auto-change-prs-branch.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Make sure new PRs are sent to development
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened ]
|
||||
|
||||
jobs:
|
||||
check-branch:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: Vankka/pr-target-branch-action@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
target: main
|
||||
exclude: dev
|
||||
change-to: dev
|
||||
comment: |
|
||||
Your PR's base branch was set to `main`, PRs should be set to target `dev`.
|
||||
The base branch of this PR has been automatically changed to `dev`, please check that there are no merge conflicts.
|
||||
already-exists-action: close_this
|
||||
already-exists-comment: "Closing, because {url} basing on the same branch"
|
||||
19
.github/workflows/build-zip.yml
vendored
Normal file
19
.github/workflows/build-zip.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Build And Upload Extension Zip Via Artifact
|
||||
|
||||
on: [ pull_request_target ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile --prefer-offline
|
||||
- run: pnpm build
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: dist/*
|
||||
42
.github/workflows/cancel-other-workflows-on-close.yml
vendored
Normal file
42
.github/workflows/cancel-other-workflows-on-close.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Cancel PR workflows on close
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ closed ]
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
if: ${{ github.event.pull_request.merged == false }}
|
||||
steps:
|
||||
- name: Cancel all queued/in_progress workflows for this PR
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
owner="${GITHUB_REPOSITORY%%/*}"
|
||||
repo="${GITHUB_REPOSITORY##*/}"
|
||||
branch="$BRANCH"
|
||||
token="${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
runs=$(curl -s -H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/$owner/$repo/actions/runs?branch=$branch&status=queued" | jq -r '.workflow_runs[].id')
|
||||
|
||||
runs+=" "
|
||||
runs+=$(curl -s -H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/$owner/$repo/actions/runs?branch=$branch&status=in_progress" | jq -r '.workflow_runs[].id')
|
||||
|
||||
for run_id in $runs; do
|
||||
if [ -n "$run_id" ]; then
|
||||
echo "Cancelling workflow run: $run_id"
|
||||
curl -s -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/$owner/$repo/actions/runs/$run_id/cancel"
|
||||
fi
|
||||
done
|
||||
40
.github/workflows/codeql.yml
vendored
Normal file
40
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "dev" ]
|
||||
pull_request:
|
||||
branches: [ "main", "dev" ]
|
||||
schedule:
|
||||
- cron: '40 18 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
security-events: write
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
30
.github/workflows/dependencies-auto-merge.yml
vendored
Normal file
30
.github/workflows/dependencies-auto-merge.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Dependabot Pull Request Approve and Merge
|
||||
on: pull_request_target
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: dependabot-metadata
|
||||
uses: dependabot/fetch-metadata@v2
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Approve a PR
|
||||
run: gh pr review --approve "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }}
|
||||
run: gh pr merge --auto --squash "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
71
.github/workflows/e2e-modular.yml
vendored
Normal file
71
.github/workflows/e2e-modular.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Modular E2E Tests Matrix
|
||||
|
||||
on: [ pull_request_target ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
ALL_SCENARIOS: '["content", "content-ui", "content-runtime", "background", "new-tab", "popup", "devtools", "side-panel", "options"]'
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
name: '${{ matrix.browser }} | Scenario: ${{ matrix.scenario }}'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: ['chrome', 'firefox']
|
||||
scenario: ['content', 'content-ui', 'content-runtime', 'background', 'new-tab', 'popup', 'devtools', 'side-panel', 'options']
|
||||
|
||||
env:
|
||||
CEB_CI: true
|
||||
CURRENT_SCENARIO: ${{ matrix.scenario }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile --prefer-offline
|
||||
|
||||
- name: Configure modules | scenario "${{ matrix.scenario }}"
|
||||
run: |
|
||||
echo "Deleting all features except '${{ matrix.scenario }}'"
|
||||
pnpm module-manager --de ${{ matrix.scenario }} tests
|
||||
|
||||
- name: Run E2E tests (${{ matrix.browser }})
|
||||
run: pnpm e2e${{ matrix.browser == 'firefox' && ':firefox' || '' }}
|
||||
|
||||
- name: Restore one random feature and re-run E2E tests
|
||||
run: |
|
||||
echo "ALL_SCENARIOS=$ALL_SCENARIOS"
|
||||
echo "CURRENT_SCENARIO=$CURRENT_SCENARIO"
|
||||
|
||||
scenarios=($(echo "$ALL_SCENARIOS" | jq -r '.[]'))
|
||||
|
||||
# Filter out current scenario
|
||||
filtered_scenarios=()
|
||||
for scenario in "${scenarios[@]}"; do
|
||||
if [ "$scenario" != "$CURRENT_SCENARIO" ]; then
|
||||
filtered_scenarios+=("$scenario")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#filtered_scenarios[@]} -eq 0 ]; then
|
||||
echo "::warning::No other scenario available, skipping restoration."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
random_feature="${filtered_scenarios[$RANDOM % ${#filtered_scenarios[@]}]}"
|
||||
|
||||
echo "Restoring feature: $random_feature"
|
||||
pnpm module-manager -r "$random_feature"
|
||||
|
||||
- name: Run again E2E tests (${{ matrix.browser }})
|
||||
run: pnpm ${{ matrix.browser == 'chrome' && 'e2e' || 'e2e:firefox' }}
|
||||
|
||||
25
.github/workflows/e2e.yml
vendored
Normal file
25
.github/workflows/e2e.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Run E2E Tests
|
||||
|
||||
on: [ pull_request_target ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: E2E tests on ${{ matrix.browser }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [chrome, firefox]
|
||||
env:
|
||||
CEB_CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile --prefer-offline
|
||||
- run: pnpm ${{ matrix.browser == 'chrome' && 'e2e' || 'e2e:firefox' }}
|
||||
16
.github/workflows/greetings.yml
vendored
Normal file
16
.github/workflows/greetings.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Greetings
|
||||
|
||||
on: [ pull_request_target, issues ]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.'
|
||||
pr-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.'
|
||||
17
.github/workflows/lint.yml
vendored
Normal file
17
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Lint Check
|
||||
|
||||
on: [ pull_request_target ]
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile --prefer-offline
|
||||
- run: pnpm lint
|
||||
25
.github/workflows/prettier.yml
vendored
Normal file
25
.github/workflows/prettier.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Formating validation
|
||||
|
||||
on: [ pull_request_target ]
|
||||
|
||||
jobs:
|
||||
prettier:
|
||||
name: Prettier Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Prettier
|
||||
id: prettier-run
|
||||
uses: rutajdash/prettier-cli-action@v1.0.2
|
||||
with:
|
||||
config_path: ./.prettierrc
|
||||
file_pattern: "*.{js,jsx,ts,tsx,json}"
|
||||
|
||||
- name: Prettier Output
|
||||
if: ${{ failure() }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "The following files aren't formatted properly:"
|
||||
echo "${{steps.prettier-run.outputs.prettier_output}}"
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# dependencies
|
||||
**/node_modules
|
||||
|
||||
# testing
|
||||
**/coverage
|
||||
|
||||
# build
|
||||
**/dist
|
||||
**/build
|
||||
**/dist-zip
|
||||
chrome-extension/manifest.js
|
||||
chrome-extension/pre-build.tsconfig.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# env
|
||||
**/.env.*
|
||||
**/.env
|
||||
|
||||
# etc
|
||||
.DS_Store
|
||||
.idea
|
||||
**/.turbo
|
||||
|
||||
# compiled
|
||||
**/tailwind-output.css
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
pnpm dlx lint-staged --allow-empty
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
public-hoist-pattern[]=@testing-library/dom
|
||||
engine-strict=true
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
*.md
|
||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"bracketSameLine": true,
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"plugins": [
|
||||
"prettier-plugin-tailwindcss"
|
||||
]
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 Seo Jong Hak
|
||||
|
||||
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.
|
||||
275
README.md
Executable file
275
README.md
Executable file
@@ -0,0 +1,275 @@
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/99cb6303-64e4-4bed-bf3f-35735353e6de" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/a5dbf71c-c509-4c4f-80f4-be88a1943b0a" />
|
||||
<img alt="Logo" src="https://github.com/user-attachments/assets/99cb6303-64e4-4bed-bf3f-35735353e6de" />
|
||||
</picture>
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
<a href="https://discord.gg/4ERQ6jgV9a" target="_blank"><img src="https://discord.com/api/guilds/1263404974830915637/widget.png"/></a>
|
||||
|
||||
> This boilerplate
|
||||
> has [Legacy version](https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/tree/legacy)
|
||||
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> This project is listed in the [Awesome Vite](https://github.com/vitejs/awesome-vite)
|
||||
|
||||
> [!TIP]
|
||||
> Share storage state between all pages
|
||||
>
|
||||
> https://github.com/user-attachments/assets/3b8e189f-6443-490e-a455-4f9570267f8c
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Intro](#intro)
|
||||
- [Features](#features)
|
||||
- [Structure](#structure)
|
||||
- [ChromeExtension](#structure-chrome-extension)
|
||||
- [Packages](#structure-packages)
|
||||
- [Pages](#structure-pages)
|
||||
- [Installation](#installation)
|
||||
- [Chrome](#installation-chrome)
|
||||
- [Firefox](#installation-firefox)
|
||||
- [Install dependency](#install-dependency)
|
||||
- [For root](#install-dependency-for-root)
|
||||
- [For module](#install-dependency-for-module)
|
||||
- [Environment variables](#env-variables)
|
||||
- [Add new](#env-variables-new)
|
||||
- [Set via CLI](#env-variables-cli-set)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Hot module reload seems to have frozen](#hot-module-reload-seems-to-have-frozen)
|
||||
- [Imports not resolving correctly](#imports-not-resolving-correctly)
|
||||
- [Community](#community)
|
||||
- [Debugging](#debugging)
|
||||
- [Reference](#reference)
|
||||
- [Star History](#star-history)
|
||||
- [Contributors](#contributors)
|
||||
|
||||
## Intro
|
||||
|
||||
This boilerplate helps you create Chrome/Firefox extensions using React and Typescript. It improves
|
||||
the build speed and development experience by using Vite and Turborepo.
|
||||
|
||||
## Features
|
||||
|
||||
- [React](https://reactjs.org/)
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Tailwindcss](https://tailwindcss.com/)
|
||||
- [Vite](https://vitejs.dev/) with [Rollup](https://rollupjs.org/)
|
||||
- [Turborepo](https://turbo.build/repo)
|
||||
- [Prettier](https://prettier.io/)
|
||||
- [ESLint](https://eslint.org/)
|
||||
- [Chrome Extensions Manifest Version 3](https://developer.chrome.com/docs/extensions/mv3/intro/)
|
||||
- [Custom i18n package](/packages/i18n/)
|
||||
- [Custom HMR (Hot Module Rebuild) plugin](/packages/hmr)
|
||||
- [End-to-end testing with WebdriverIO](https://webdriver.io/)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository.( ```git clone https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite``` )
|
||||
2. Ensure your node version is >= than in `.nvmrc` file, recommend to use [nvm](https://github.com/nvm-sh/nvm?tab=readme-ov-file#intro)
|
||||
3. Edit `/packages/i18n/locales/`{your locale(s)}/`messages.json`
|
||||
4. In the objects `extensionDescription` and `extensionName`, change the `message` fields (leave `description` alone)
|
||||
5. Install pnpm globally: `npm install -g pnpm`
|
||||
6. Run `pnpm install`
|
||||
7. Check if you have that configuration in your IDE/Editor:
|
||||
- <b>VS Code</b>:
|
||||
- Installed [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
- Installed [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
- Enabled `Typescript Workbench version` in settings:
|
||||
- CTRL + SHIFT + P -> Search: `Typescript: Select Typescript version...` -> `Use Workbench version`
|
||||
- [Read more](https://code.visualstudio.com/docs/languages/typescript#_using-newer-typescript-versions)
|
||||
- Optional, for imports to work correctly in WSL, you might need to install the [Remote - WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) extension and connect to WSL remotely from VS Code. See overview section in the extension page for more information.
|
||||
- <b>WebStorm</b>:
|
||||
- Configured [ESLint](https://www.jetbrains.com/help/webstorm/eslint.html#ws_eslint_configure_run_eslint_on_save)
|
||||
- Configured [Prettier](https://prettier.io/docs/en/webstorm.html)
|
||||
- Optional, but useful `File | Settings | Tools | Actions on Save`\
|
||||
-> `Optimize imports` and `Reformat code`
|
||||
8. Run `pnpm update-version <version>` for change the `version` to the desired version of your extension.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> On Windows, make sure you have WSL enabled and Linux distribution (e.g. Ubuntu) installed on WSL.
|
||||
>
|
||||
> [Installation Guide](https://learn.microsoft.com/en-us/windows/wsl/install)
|
||||
|
||||
<b>Then, depending on the target browser:</b>
|
||||
|
||||
### For Chrome: <a name="installation-chrome"></a>
|
||||
|
||||
1. Run:
|
||||
- Dev: `pnpm dev` (on Windows, you should run as administrator;
|
||||
see [issue#456](https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/issues/456))
|
||||
- Prod: `pnpm build`
|
||||
2. Open in browser - `chrome://extensions`
|
||||
3. Check - <kbd>Developer mode</kbd>
|
||||
4. Click - <kbd>Load unpacked</kbd> in the upper left corner
|
||||
5. Select the `dist` directory from the boilerplate project
|
||||
|
||||
### For Firefox: <a name="installation-firefox"></a>
|
||||
|
||||
1. Run:
|
||||
- Dev: `pnpm dev:firefox`
|
||||
- Prod: `pnpm build:firefox`
|
||||
2. Open in browser - `about:debugging#/runtime/this-firefox`
|
||||
3. Click - <kbd>Load Temporary Add-on...</kbd> in the upper right corner
|
||||
4. Select the `./dist/manifest.json` file from the boilerplate project
|
||||
|
||||
> [!NOTE]
|
||||
> In Firefox, you load add-ons in temporary mode. That means they'll disappear after each browser close. You have to
|
||||
> load the add-on on every browser launch.
|
||||
|
||||
## Install dependency for turborepo: <a name="install-dependency"></a>
|
||||
|
||||
### For root: <a name="install-dependency-for-root"></a>
|
||||
|
||||
1. Run `pnpm i <package> -w`
|
||||
|
||||
### For module: <a name="install-dependency-for-module"></a>
|
||||
|
||||
1. Run `pnpm i <package> -F <module name>`
|
||||
|
||||
`package` - Name of the package you want to install e.g. `nodemon` \
|
||||
`module-name` - You can find it inside each `package.json` under the key `name`, e.g. `@extension/content-script`, you
|
||||
can use only `content-script` without `@extension/` prefix
|
||||
|
||||
## How do I disable modules I'm not using?
|
||||
|
||||
[Read here](packages/module-manager/README.md)
|
||||
|
||||
## Environment variables
|
||||
|
||||
Read: [Env Documentation](packages/env/README.md)
|
||||
|
||||
## Boilerplate structure <a name="structure"></a>
|
||||
|
||||
### Chrome extension <a name="structure-chrome-extension"></a>
|
||||
|
||||
The extension lives in the `chrome-extension` directory and includes the following files:
|
||||
|
||||
- [`manifest.ts`](chrome-extension/manifest.ts) - script that outputs the `manifest.json`
|
||||
- [`src/background`](chrome-extension/src/background) - [background script](https://developer.chrome.com/docs/extensions/mv3/background_pages/)
|
||||
(`background.service_worker` in manifest.json)
|
||||
- [`public`](chrome-extension/public/) - icons referenced in the manifest; content CSS for user's page injection
|
||||
|
||||
> [!IMPORTANT]
|
||||
> To facilitate development, the boilerplate is configured to "Read and change all your data on all websites".
|
||||
> In production, it's best practice to limit the premissions to only the strictly necessary websites. See
|
||||
> [Declaring permissions](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions)
|
||||
> and edit `manifest.js` accordingly.
|
||||
|
||||
### Pages <a name="structure-pages"></a>
|
||||
|
||||
Code that is transpiled to be part of the extension lives in the [pages](pages) directory.
|
||||
|
||||
- [`content`](pages/content) - Scripts injected into specified pages (You can see it in console)
|
||||
- [`content-ui`](pages/content-ui) - React Components injected into specified pages (You can see it at the very bottom of pages)
|
||||
- [`content-runtime`](pages/content-runtime/src/) - [injected content scripts](https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#functionality)
|
||||
This can be injected from e.g. `popup` like standard `content`
|
||||
- [`devtools`](pages/devtools/) - [extend the browser DevTools](https://developer.chrome.com/docs/extensions/how-to/devtools/extend-devtools#creating)
|
||||
(`devtools_page` in manifest.json)
|
||||
- [`devtools-panel`](pages/devtools-panel/) - [DevTools panel](https://developer.chrome.com/docs/extensions/reference/api/devtools/panels)
|
||||
for [devtools](pages/devtools/src/index.ts)
|
||||
- [`new-tab`](pages/new-tab/) - [override the default New Tab page](https://developer.chrome.com/docs/extensions/develop/ui/override-chrome-pages)
|
||||
(`chrome_url_overrides.newtab` in manifest.json)
|
||||
- [`options`](pages/options/) - [options page](https://developer.chrome.com/docs/extensions/develop/ui/options-page)
|
||||
(`options_page` in manifest.json)
|
||||
- [`popup`](pages/popup/) - [popup](https://developer.chrome.com/docs/extensions/reference/api/action#popup) shown when
|
||||
clicking the extension in the toolbar
|
||||
(`action.default_popup` in manifest.json)
|
||||
- [`side-panel`](pages/side-panel/) - [sidepanel (Chrome 114+)](https://developer.chrome.com/docs/extensions/reference/api/sidePanel)
|
||||
(`side_panel.default_path` in manifest.json)
|
||||
|
||||
### Packages <a name="structure-packages"></a>
|
||||
|
||||
Some shared packages:
|
||||
|
||||
- `dev-utils` - utilities for Chrome extension development (manifest-parser, logger)
|
||||
- `env` - exports object which contain all environment variables from `.env` and dynamically declared
|
||||
- `hmr` - custom HMR plugin for Vite, injection script for reload/refresh, HMR dev-server
|
||||
- `i18n` - custom internationalization package; provides i18n function with type safety and other validation
|
||||
- `shared` - shared code for the entire project (types, constants, custom hooks, components etc.)
|
||||
- `storage` - helpers for easier integration with [storage](https://developer.chrome.com/docs/extensions/reference/api/storage), e.g. local/session storages
|
||||
- `tailwind-config` - shared Tailwind config for entire project
|
||||
- `tsconfig` - shared tsconfig for the entire project
|
||||
- `ui` - function to merge your Tailwind config with the global one; you can save components here
|
||||
- `vite-config` - shared Vite config for the entire project
|
||||
|
||||
Other useful packages:
|
||||
|
||||
- `zipper` - run `pnpm zip` to pack the `dist` folder into `extension-YYYYMMDD-HHmmss.zip` inside the newly created
|
||||
`dist-zip`
|
||||
- `module-manager` - run `pnpm module-manager` to enable/disable modules
|
||||
- `e2e` - run `pnpm e2e` for end-to-end tests of your zipped extension on different browsers
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hot module reload seems to have frozen
|
||||
|
||||
If saving source files doesn't cause the extension HMR code to trigger a reload of the browser page, try this:
|
||||
|
||||
1. Ctrl+C the development server and restart it (`pnpm run dev`)
|
||||
2. If you get a [`grpc` error](https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/issues/612),
|
||||
[kill the
|
||||
`turbo` process](https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/issues/612#issuecomment-2518982339)
|
||||
and run `pnpm dev` again.
|
||||
|
||||
### Imports not resolving correctly
|
||||
|
||||
If you are using WSL and imports are not resolving correctly, ensure that you have connected VS Code to WSL remotely using the [Remote - WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) extension.
|
||||
|
||||
## Community
|
||||
|
||||
To chat with other community members, you can join the [Discord](https://discord.gg/4ERQ6jgV9a) server.
|
||||
You can ask questions on that server, and you can also help others.
|
||||
|
||||
Also, suggest new features or share any challenges you've faced while developing Chrome extensions!
|
||||
|
||||
## Debugging
|
||||
|
||||
If you're debugging one, you can use [Brie](https://go.briehq.com/github?utm_source=CEB) lets you capture screenshots, errors, and network activity, making it easier for us to help.
|
||||
|
||||
## Reference
|
||||
|
||||
- [Chrome Extensions](https://developer.chrome.com/docs/extensions)
|
||||
- [Vite Plugin](https://vitejs.dev/guide/api-plugin.html)
|
||||
- [Rollup](https://rollupjs.org/guide/en/)
|
||||
- [Turborepo](https://turbo.build/repo/docs)
|
||||
- [Rollup-plugin-chrome-extension](https://www.extend-chrome.dev/rollup-plugin)
|
||||
|
||||
## Star History <a name="star-history"></a>
|
||||
|
||||
<a href="https://star-history.com/#Jonghakseo/chrome-extension-boilerplate-react-vite&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Jonghakseo/chrome-extension-boilerplate-react-vite&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Jonghakseo/chrome-extension-boilerplate-react-vite&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Jonghakseo/chrome-extension-boilerplate-react-vite&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contributors <a name="contributors"></a>
|
||||
|
||||
This Boilerplate is made possible thanks to all of its contributors.
|
||||
|
||||
<a href="https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/graphs/contributors">
|
||||
<img width="500px" src="https://contrib.rocks/image?repo=Jonghakseo/chrome-extension-boilerplate-react-vite" alt="All Contributors"/>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## Special Thanks To
|
||||
|
||||
| <a href="https://jb.gg/OpenSourceSupport"><img width="40" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo."></a> | <a href="https://www.linkedin.com/in/j-acks0n"><img width="40" style="border-radius:50%" src='https://avatars.githubusercontent.com/u/23139754' alt='Jackson Hong'/></a> |
|
||||
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|
||||
---
|
||||
|
||||
Made by [Jonghakseo](https://jonghakseo.github.io/)
|
||||
8
UPDATE-PACKAGE-VERSIONS.md
Normal file
8
UPDATE-PACKAGE-VERSIONS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
For update package version in all ```package.json``` files use this command in root:
|
||||
|
||||
FOR WINDOWS YOU NEED TO USE E.G ```GIT BASH``` CONSOLE OR OTHER WHICH SUPPORT UNIX COMMANDS
|
||||
```bash
|
||||
pnpm update-version <new_version>
|
||||
```
|
||||
|
||||
If script was run successfully you will see ```Updated versions to <new_version>```
|
||||
8
bash-scripts/copy_env.sh
Normal file
8
bash-scripts/copy_env.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if .env does not exist and .example.env exists
|
||||
if [ ! -f ".env" ] && [ -f ".example.env" ]; then
|
||||
# Copy .example.env to .env
|
||||
cp .example.env .env
|
||||
echo ".example.env has been copied to .env"
|
||||
fi
|
||||
91
bash-scripts/set_global_env.sh
Normal file
91
bash-scripts/set_global_env.sh
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Default values
|
||||
CLI_CEB_DEV=false
|
||||
CLI_CEB_FIREFOX=false
|
||||
|
||||
validate_is_boolean() {
|
||||
if [[ "$1" != "true" && "$1" != "false" ]]; then
|
||||
echo "Invalid value for <$2>. Please use 'true' or 'false'."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate if a key starts with CLI_CEB_ or CEB_
|
||||
validate_key() {
|
||||
local key="$1"
|
||||
local is_editable_section="${2:-false}"
|
||||
|
||||
if [[ -n "$key" && ! "$key" =~ ^# ]]; then
|
||||
if [[ "$is_editable_section" == true && ! "$key" =~ ^CEB_ ]]; then
|
||||
echo "Invalid key: <$key>. All keys in the editable section must start with 'CEB_'."
|
||||
exit 1
|
||||
elif [[ "$is_editable_section" == false && ! "$key" =~ ^CLI_CEB_ ]]; then
|
||||
echo "Invalid key: <$key>. All CLI keys must start with 'CLI_CEB_'."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
parse_arguments() {
|
||||
for arg in "$@"
|
||||
do
|
||||
key="${arg%%=*}"
|
||||
value="${arg#*=}"
|
||||
|
||||
validate_key "$key"
|
||||
|
||||
case $key in
|
||||
CLI_CEB_DEV)
|
||||
CLI_CEB_DEV="$value"
|
||||
validate_is_boolean "$CLI_CEB_DEV" "CLI_CEB_DEV"
|
||||
;;
|
||||
CLI_CEB_FIREFOX)
|
||||
CLI_CEB_FIREFOX="$value"
|
||||
validate_is_boolean "$CLI_CEB_FIREFOX" "CLI_CEB_FIREFOX"
|
||||
;;
|
||||
*)
|
||||
cli_values+=("$key=$value")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Validate keys in .env file
|
||||
validate_env_keys() {
|
||||
editable_section_starts=false
|
||||
|
||||
while IFS= read -r line; do
|
||||
key="${line%%=*}"
|
||||
if [[ "$key" =~ ^CLI_CEB_ ]]; then
|
||||
editable_section_starts=true
|
||||
elif $editable_section_starts; then
|
||||
validate_key "$key" true
|
||||
fi
|
||||
done < .env
|
||||
}
|
||||
|
||||
create_new_file() {
|
||||
temp_file=$(mktemp)
|
||||
|
||||
{
|
||||
echo "# THOSE VALUES ARE EDITABLE ONLY VIA CLI"
|
||||
echo "CLI_CEB_DEV=$CLI_CEB_DEV"
|
||||
echo "CLI_CEB_FIREFOX=$CLI_CEB_FIREFOX"
|
||||
for value in "${cli_values[@]}"; do
|
||||
echo "$value"
|
||||
done
|
||||
echo ""
|
||||
echo "# THOSE VALUES ARE EDITABLE"
|
||||
|
||||
# Copy existing env values, without CLI section
|
||||
grep -E '^CEB_' .env
|
||||
} > "$temp_file"
|
||||
|
||||
mv "$temp_file" .env
|
||||
}
|
||||
|
||||
# Main script execution
|
||||
parse_arguments "$@"
|
||||
validate_env_keys
|
||||
create_new_file
|
||||
17
bash-scripts/update_version.sh
Normal file
17
bash-scripts/update_version.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Usage: ./update_version.sh <new_version>
|
||||
# FORMAT IS <0.0.0>
|
||||
|
||||
if [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
find . -name 'package.json' -not -path '*/node_modules/*' -exec bash -c '
|
||||
# Parse the version from package.json
|
||||
current_version=$(grep -o "\"version\": \"[^\"]*" "$0" | cut -d"\"" -f4)
|
||||
|
||||
# Update the version
|
||||
perl -i -pe"s/$current_version/'$1'/" "$0"
|
||||
' {} \;
|
||||
|
||||
echo "Updated versions to $1";
|
||||
else
|
||||
echo "Version format <$1> isn't correct, proper format is <0.0.0>";
|
||||
fi
|
||||
84
chrome-extension/manifest.ts
Normal file
84
chrome-extension/manifest.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import type { ManifestType } from '@extension/shared';
|
||||
|
||||
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
|
||||
/**
|
||||
* @prop default_locale
|
||||
* if you want to support multiple languages, you can use the following reference
|
||||
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization
|
||||
*
|
||||
* @prop browser_specific_settings
|
||||
* Must be unique to your extension to upload to addons.mozilla.org
|
||||
* (you can delete if you only want a chrome extension)
|
||||
*
|
||||
* @prop permissions
|
||||
* Firefox doesn't support sidePanel (It will be deleted in manifest parser)
|
||||
*
|
||||
* @prop content_scripts
|
||||
* css: ['content.css'], // public folder
|
||||
*/
|
||||
const manifest = {
|
||||
manifest_version: 3,
|
||||
default_locale: 'en',
|
||||
name: '__MSG_extensionName__',
|
||||
browser_specific_settings: {
|
||||
gecko: {
|
||||
id: 'example@example.com',
|
||||
strict_min_version: '109.0',
|
||||
},
|
||||
},
|
||||
version: packageJson.version,
|
||||
description: '__MSG_extensionDescription__',
|
||||
host_permissions: ['<all_urls>'],
|
||||
permissions: ['storage', 'scripting', 'tabs', 'notifications', 'sidePanel'],
|
||||
options_page: 'options/index.html',
|
||||
background: {
|
||||
service_worker: 'background.js',
|
||||
type: 'module',
|
||||
},
|
||||
action: {
|
||||
default_popup: 'popup/index.html',
|
||||
default_icon: 'icon-34.png',
|
||||
},
|
||||
chrome_url_overrides: {
|
||||
newtab: 'new-tab/index.html',
|
||||
},
|
||||
icons: {
|
||||
'128': 'icon-128.png',
|
||||
},
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ['http://*/*', 'https://*/*', '<all_urls>'],
|
||||
js: ['content/all.iife.js'],
|
||||
},
|
||||
{
|
||||
matches: ['https://example.com/*'],
|
||||
js: ['content/example.iife.js'],
|
||||
},
|
||||
{
|
||||
matches: ['http://*/*', 'https://*/*', '<all_urls>'],
|
||||
js: ['content-ui/all.iife.js'],
|
||||
},
|
||||
{
|
||||
matches: ['https://example.com/*'],
|
||||
js: ['content-ui/example.iife.js'],
|
||||
},
|
||||
{
|
||||
matches: ['http://*/*', 'https://*/*', '<all_urls>'],
|
||||
css: ['content.css'],
|
||||
},
|
||||
],
|
||||
devtools_page: 'devtools/index.html',
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['*.js', '*.css', '*.svg', 'icon-128.png', 'icon-34.png'],
|
||||
matches: ['*://*/*'],
|
||||
},
|
||||
],
|
||||
side_panel: {
|
||||
default_path: 'side-panel/index.html',
|
||||
},
|
||||
} satisfies ManifestType;
|
||||
|
||||
export default manifest;
|
||||
36
chrome-extension/package.json
Normal file
36
chrome-extension/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "chrome-extension",
|
||||
"version": "0.5.0",
|
||||
"description": "chrome extension - core settings",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"clean:node_modules": "pnpx rimraf node_modules",
|
||||
"clean:turbo": "rimraf .turbo",
|
||||
"clean": "pnpm clean:turbo && pnpm clean:node_modules",
|
||||
"ready": "tsc -b pre-build.tsconfig.json",
|
||||
"build": "vite build",
|
||||
"dev": "vite build --mode development",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"prettier": "prettier . --write --ignore-path ../.prettierignore",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@extension/env": "workspace:*",
|
||||
"@extension/shared": "workspace:*",
|
||||
"@extension/storage": "workspace:*",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@extension/dev-utils": "workspace:*",
|
||||
"@extension/hmr": "workspace:*",
|
||||
"@extension/tsconfig": "workspace:*",
|
||||
"@extension/vite-config": "workspace:*",
|
||||
"@laynezh/vite-plugin-lib-assets": "^1.1.0",
|
||||
"magic-string": "^0.30.17",
|
||||
"ts-loader": "^9.5.2"
|
||||
}
|
||||
}
|
||||
9
chrome-extension/pre-build.tsconfig.json
Normal file
9
chrome-extension/pre-build.tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["manifest.ts"],
|
||||
"exclude": [""]
|
||||
}
|
||||
0
chrome-extension/public/content.css
Normal file
0
chrome-extension/public/content.css
Normal file
BIN
chrome-extension/public/icon-128.png
Normal file
BIN
chrome-extension/public/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
chrome-extension/public/icon-34.png
Normal file
BIN
chrome-extension/public/icon-34.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
9
chrome-extension/src/background/index.ts
Normal file
9
chrome-extension/src/background/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'webextension-polyfill';
|
||||
import { exampleThemeStorage } from '@extension/storage';
|
||||
|
||||
exampleThemeStorage.get().then(theme => {
|
||||
console.log('theme', theme);
|
||||
});
|
||||
|
||||
console.log('Background loaded');
|
||||
console.log("Edit 'chrome-extension/src/background/index.ts' and save to reload.");
|
||||
10
chrome-extension/tsconfig.json
Normal file
10
chrome-extension/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@extension/tsconfig/app",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "utils", "vite.config.mts", "manifest.ts", "../node_modules/@types"]
|
||||
}
|
||||
83
chrome-extension/utils/plugins/make-manifest-plugin.ts
Normal file
83
chrome-extension/utils/plugins/make-manifest-plugin.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ManifestParser } from '@extension/dev-utils';
|
||||
import { IS_DEV, IS_FIREFOX } from '@extension/env';
|
||||
import { colorfulLog } from '@extension/shared';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { platform } from 'node:process';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import type { ManifestType } from '@extension/shared';
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
const manifestFile = resolve(import.meta.dirname, '..', '..', 'manifest.js');
|
||||
const refreshFilePath = resolve(
|
||||
import.meta.dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'packages',
|
||||
'hmr',
|
||||
'dist',
|
||||
'lib',
|
||||
'injections',
|
||||
'refresh.js',
|
||||
);
|
||||
|
||||
const withHMRId = (code: string) => `(function() {let __HMR_ID = 'chrome-extension-hmr';${code}\n})();`;
|
||||
|
||||
const getManifestWithCacheBurst = async () => {
|
||||
const withCacheBurst = (path: string) => `${path}?${Date.now().toString()}`;
|
||||
|
||||
/**
|
||||
* In Windows, import() doesn't work without file:// protocol.
|
||||
* So, we need to convert path to file:// protocol. (url.pathToFileURL)
|
||||
*/
|
||||
if (platform === 'win32') {
|
||||
return (await import(withCacheBurst(pathToFileURL(manifestFile).href))).default;
|
||||
} else {
|
||||
return (await import(withCacheBurst(manifestFile))).default;
|
||||
}
|
||||
};
|
||||
|
||||
const addRefreshContentScript = (manifest: ManifestType) => {
|
||||
manifest.content_scripts = manifest.content_scripts || [];
|
||||
manifest.content_scripts.push({
|
||||
matches: ['http://*/*', 'https://*/*', '<all_urls>'],
|
||||
js: ['refresh.js'], // for public's HMR(refresh) support
|
||||
});
|
||||
};
|
||||
|
||||
export default (config: { outDir: string }): PluginOption => {
|
||||
const makeManifest = (manifest: ManifestType, to: string) => {
|
||||
if (!existsSync(to)) {
|
||||
mkdirSync(to);
|
||||
}
|
||||
|
||||
const manifestPath = resolve(to, 'manifest.json');
|
||||
|
||||
if (IS_DEV) {
|
||||
addRefreshContentScript(manifest);
|
||||
}
|
||||
|
||||
writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, IS_FIREFOX));
|
||||
|
||||
const refreshFileString = readFileSync(refreshFilePath, 'utf-8');
|
||||
|
||||
if (IS_DEV) {
|
||||
writeFileSync(resolve(to, 'refresh.js'), withHMRId(refreshFileString));
|
||||
}
|
||||
|
||||
colorfulLog(`Manifest file copy complete: ${manifestPath}`, 'success');
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'make-manifest',
|
||||
buildStart() {
|
||||
this.addWatchFile(manifestFile);
|
||||
},
|
||||
async writeBundle() {
|
||||
const outDir = config.outDir;
|
||||
const manifest = await getManifestWithCacheBurst();
|
||||
makeManifest(manifest, outDir);
|
||||
},
|
||||
};
|
||||
};
|
||||
52
chrome-extension/vite.config.mts
Normal file
52
chrome-extension/vite.config.mts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { defineConfig, type PluginOption } from 'vite';
|
||||
import libAssetsPlugin from '@laynezh/vite-plugin-lib-assets';
|
||||
import makeManifestPlugin from './utils/plugins/make-manifest-plugin.js';
|
||||
import { watchPublicPlugin, watchRebuildPlugin } from '@extension/hmr';
|
||||
import { watchOption } from '@extension/vite-config';
|
||||
import env, { IS_DEV, IS_PROD } from '@extension/env';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
|
||||
const rootDir = resolve(import.meta.dirname);
|
||||
const srcDir = resolve(rootDir, 'src');
|
||||
|
||||
const outDir = resolve(rootDir, '..', 'dist');
|
||||
export default defineConfig({
|
||||
define: {
|
||||
'process.env': env,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@root': rootDir,
|
||||
'@src': srcDir,
|
||||
'@assets': resolve(srcDir, 'assets'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
libAssetsPlugin({
|
||||
outputPath: outDir,
|
||||
}) as PluginOption,
|
||||
watchPublicPlugin(),
|
||||
makeManifestPlugin({ outDir }),
|
||||
IS_DEV && watchRebuildPlugin({ reload: true, id: 'chrome-extension-hmr' }),
|
||||
nodePolyfills(),
|
||||
],
|
||||
publicDir: resolve(rootDir, 'public'),
|
||||
build: {
|
||||
lib: {
|
||||
name: 'BackgroundScript',
|
||||
fileName: 'background',
|
||||
formats: ['es'],
|
||||
entry: resolve(srcDir, 'background', 'index.ts'),
|
||||
},
|
||||
outDir,
|
||||
emptyOutDir: false,
|
||||
sourcemap: IS_DEV,
|
||||
minify: IS_PROD,
|
||||
reportCompressedSize: IS_PROD,
|
||||
watch: watchOption,
|
||||
rollupOptions: {
|
||||
external: ['chrome'],
|
||||
},
|
||||
},
|
||||
});
|
||||
105
eslint.config.ts
Normal file
105
eslint.config.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { fixupConfigRules } from '@eslint/compat';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import js from '@eslint/js';
|
||||
import { flatConfigs as importXFlatConfig } from 'eslint-plugin-import-x';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import { browser, es2020, node } from 'globals';
|
||||
import { config, configs as tsConfigs, parser as tsParser } from 'typescript-eslint';
|
||||
import type { FixupConfigArray } from '@eslint/compat';
|
||||
|
||||
export default config(
|
||||
// Shared configs
|
||||
js.configs.recommended,
|
||||
...tsConfigs.recommended,
|
||||
jsxA11y.flatConfigs.recommended,
|
||||
importXFlatConfig.recommended,
|
||||
importXFlatConfig.typescript,
|
||||
eslintPluginPrettierRecommended,
|
||||
...fixupConfigRules(new FlatCompat().extends('plugin:react-hooks/recommended') as FixupConfigArray),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
...reactPlugin.configs.flat.recommended,
|
||||
...reactPlugin.configs.flat['jsx-runtime'],
|
||||
},
|
||||
// Custom config
|
||||
{
|
||||
ignores: ['**/build/**', '**/dist/**', '**/node_modules/**', 'chrome-extension/manifest.js'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
projectService: true,
|
||||
},
|
||||
globals: {
|
||||
...browser,
|
||||
...es2020,
|
||||
...node,
|
||||
chrome: 'readonly',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'prefer-const': 'error',
|
||||
'no-var': 'error',
|
||||
'func-style': ['error', 'expression', { allowArrowFunctions: true }],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
name: 'type-fest',
|
||||
message: 'Please import from `@extension/shared` instead of `type-fest`.',
|
||||
},
|
||||
],
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/consistent-type-exports': 'error',
|
||||
'import-x/order': [
|
||||
'error',
|
||||
{
|
||||
'newlines-between': 'never',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
groups: ['index', 'sibling', 'parent', 'internal', 'external', 'builtin', 'object', 'type'],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: '@*/**',
|
||||
group: 'internal',
|
||||
position: 'before',
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['type'],
|
||||
},
|
||||
],
|
||||
'import-x/no-unresolved': 'off',
|
||||
'import-x/no-named-as-default': 'error',
|
||||
'import-x/no-named-as-default-member': 'error',
|
||||
'import-x/newline-after-import': 'error',
|
||||
'import-x/no-deprecated': 'error',
|
||||
'import-x/no-duplicates': ['error', { considerQueryString: true, 'prefer-inline': false }],
|
||||
'import-x/consistent-type-specifier-style': 'error',
|
||||
'import-x/exports-last': 'error',
|
||||
'import-x/first': 'error',
|
||||
},
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'error',
|
||||
},
|
||||
},
|
||||
// Overrides Rules
|
||||
{
|
||||
files: ['**/packages/shared/**/*.ts'],
|
||||
rules: {
|
||||
'no-restricted-imports': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
97
package.json
Normal file
97
package.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "chrome-extension-boilerplate-react-vite",
|
||||
"version": "0.5.0",
|
||||
"description": "chrome extension boilerplate",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite.git"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean:bundle": "rimraf dist && turbo clean:bundle",
|
||||
"clean:node_modules": "pnpx rimraf node_modules && pnpx turbo clean:node_modules",
|
||||
"clean:turbo": "rimraf .turbo && turbo clean:turbo",
|
||||
"clean": "pnpm clean:bundle && pnpm clean:turbo && pnpm clean:node_modules",
|
||||
"clean:install": "pnpm clean:node_modules && pnpm install --frozen-lockfile",
|
||||
"type-check": "turbo type-check",
|
||||
"base-build": "pnpm clean:bundle && turbo build",
|
||||
"build": "pnpm set-global-env && pnpm base-build",
|
||||
"build:firefox": "pnpm set-global-env CLI_CEB_FIREFOX=true && pnpm base-build",
|
||||
"base-dev": "pnpm clean:bundle && turbo ready && turbo watch dev",
|
||||
"dev": "pnpm set-global-env CLI_CEB_DEV=true && pnpm base-dev",
|
||||
"dev:firefox": "pnpm set-global-env CLI_CEB_DEV=true CLI_CEB_FIREFOX=true && pnpm base-dev",
|
||||
"zip": "pnpm build && pnpm -F zipper zip",
|
||||
"zip:firefox": "pnpm build:firefox && pnpm -F zipper zip",
|
||||
"e2e": "pnpm zip && turbo e2e",
|
||||
"e2e:firefox": "pnpm zip:firefox && turbo e2e",
|
||||
"lint": "turbo lint --continue",
|
||||
"lint:fix": "turbo lint:fix --continue",
|
||||
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
|
||||
"prepare": "husky",
|
||||
"update-version": "bash bash-scripts/update_version.sh",
|
||||
"copy-env": "bash bash-scripts/copy_env.sh",
|
||||
"set-global-env": "bash bash-scripts/set_global_env.sh",
|
||||
"module-manager": "pnpm -F module-manager start",
|
||||
"postinstall": "pnpm copy-env"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@types/chrome": "^0.0.323",
|
||||
"@types/eslint-plugin-jsx-a11y": "^6.10.0",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/react": "^19.1.5",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.3.5",
|
||||
"eslint-plugin-import-x": "^4.12.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fflate": "^0.8.2",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.4.2",
|
||||
"lint-staged": "^16.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-load-config": "^6.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"rimraf": "^6.0.1",
|
||||
"run-script-os": "^1.1.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.19.4",
|
||||
"turbo": "^2.5.3",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-node-polyfills": "^0.23.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,json}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"engines": {
|
||||
"node": ">=22.15.1"
|
||||
}
|
||||
}
|
||||
16
packages/dev-utils/README.md
Normal file
16
packages/dev-utils/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Shared Package
|
||||
|
||||
This package contains code which could helps you to develop.
|
||||
To use the code in the package, you need to add the following to the package.json file.
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"@extension/dev-utils": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Information
|
||||
|
||||
`lib/shared-types.ts` contains the types from [type-fest](https://github.com/sindresorhus/type-fest?tab=readme-ov-file#install).
|
||||
1
packages/dev-utils/index.mts
Normal file
1
packages/dev-utils/index.mts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/index.js';
|
||||
2
packages/dev-utils/lib/index.ts
Normal file
2
packages/dev-utils/lib/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './stream-file-to-zip.js';
|
||||
export * from './manifest-parser/index.js';
|
||||
39
packages/dev-utils/lib/manifest-parser/impl.ts
Normal file
39
packages/dev-utils/lib/manifest-parser/impl.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { IManifestParser } from './types.js';
|
||||
import type { ManifestType } from '@extension/shared';
|
||||
|
||||
const convertToFirefoxCompatibleManifest = (manifest: ManifestType) => {
|
||||
const manifestCopy = {
|
||||
...manifest,
|
||||
} as { [key: string]: unknown };
|
||||
|
||||
if (manifest.background?.service_worker) {
|
||||
manifestCopy.background = {
|
||||
scripts: [manifest.background.service_worker],
|
||||
type: 'module',
|
||||
};
|
||||
}
|
||||
if (manifest.options_page) {
|
||||
manifestCopy.options_ui = {
|
||||
page: manifest.options_page,
|
||||
browser_style: false,
|
||||
};
|
||||
}
|
||||
manifestCopy.content_security_policy = {
|
||||
extension_pages: "script-src 'self'; object-src 'self'",
|
||||
};
|
||||
manifestCopy.permissions = (manifestCopy.permissions as string[]).filter(value => value !== 'sidePanel');
|
||||
|
||||
delete manifestCopy.options_page;
|
||||
delete manifestCopy.side_panel;
|
||||
return manifestCopy as ManifestType;
|
||||
};
|
||||
|
||||
export const ManifestParserImpl: IManifestParser = {
|
||||
convertManifestToString: (manifest, isFirefox) => {
|
||||
if (isFirefox) {
|
||||
manifest = convertToFirefoxCompatibleManifest(manifest);
|
||||
}
|
||||
|
||||
return JSON.stringify(manifest, null, 2);
|
||||
},
|
||||
};
|
||||
4
packages/dev-utils/lib/manifest-parser/index.ts
Normal file
4
packages/dev-utils/lib/manifest-parser/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { ManifestParserImpl } from './impl.js';
|
||||
|
||||
export type * from './types.js';
|
||||
export const ManifestParser = ManifestParserImpl;
|
||||
5
packages/dev-utils/lib/manifest-parser/types.ts
Normal file
5
packages/dev-utils/lib/manifest-parser/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ManifestType } from '@extension/shared';
|
||||
|
||||
export interface IManifestParser {
|
||||
convertManifestToString: (manifest: ManifestType, isFirefox: boolean) => string;
|
||||
}
|
||||
24
packages/dev-utils/lib/stream-file-to-zip.ts
Normal file
24
packages/dev-utils/lib/stream-file-to-zip.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AsyncZipDeflate } from 'fflate';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import type { Zip } from 'fflate';
|
||||
|
||||
export const streamFileToZip = (
|
||||
absPath: string,
|
||||
relPath: string,
|
||||
zip: Zip,
|
||||
onAbort: () => void,
|
||||
onError: (error: Error) => void,
|
||||
): void => {
|
||||
const data = new AsyncZipDeflate(relPath, { level: 1 });
|
||||
void zip.add(data);
|
||||
|
||||
createReadStream(absPath)
|
||||
.on('data', (chunk: string | Buffer) =>
|
||||
typeof chunk === 'string' ? data.push(Buffer.from(chunk), false) : data.push(chunk, false),
|
||||
)
|
||||
.on('end', () => data.push(new Uint8Array(0), true))
|
||||
.on('error', error => {
|
||||
onAbort();
|
||||
onError(error);
|
||||
});
|
||||
};
|
||||
29
packages/dev-utils/package.json
Normal file
29
packages/dev-utils/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@extension/dev-utils",
|
||||
"version": "0.5.0",
|
||||
"description": "chrome extension - dev utils",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"types": "index.mts",
|
||||
"main": "dist/index.mjs",
|
||||
"scripts": {
|
||||
"clean:bundle": "rimraf dist",
|
||||
"clean:node_modules": "pnpx rimraf node_modules",
|
||||
"clean:turbo": "rimraf .turbo",
|
||||
"clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo",
|
||||
"ready": "tsc -b",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"format": "prettier . --write --ignore-path ../../.prettierignore",
|
||||
"type-check": "tsc --noEmit",
|
||||
"postinstall": "pnpm ready"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@extension/tsconfig": "workspace:*",
|
||||
"@extension/shared": "workspace:*"
|
||||
}
|
||||
}
|
||||
8
packages/dev-utils/tsconfig.json
Normal file
8
packages/dev-utils/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@extension/tsconfig/module",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["index.mts", "lib"]
|
||||
}
|
||||
31
packages/env/README.md
vendored
Normal file
31
packages/env/README.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Environment Package
|
||||
|
||||
This package contains code which creates env values.
|
||||
To use the code in the package, you need to follow those steps:
|
||||
|
||||
1. Add a new record to `.env` (NEED TO CONTAIN `CEB_` PREFIX),
|
||||
|
||||
- If you want via cli:
|
||||
- Add it as argument like: `pnpm set-global-env CLI_CEB_NEXT_VALUE=new_data ...` (NEED TO CONTAIN `CLI_CEB_` PREFIX)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> `CLI_CEB_DEV` and `CLI_CEB_FIREFOX` are `false` by default \
|
||||
> All CLI values are overwriting in each call, that's mean you'll have access to values from current script run only.
|
||||
|
||||
- If you want dynamic variables go to `lib/index.ts` and edit `dynamicEnvValues` object.
|
||||
|
||||
2. Use it, for example:
|
||||
```ts
|
||||
console.log(process.env['CEB_EXAMPLE']);
|
||||
```
|
||||
or
|
||||
```ts
|
||||
console.log(process.env.CEB_EXAMPLE);
|
||||
```
|
||||
but with first solution, autofill should work for IDE:
|
||||

|
||||
3. You are also able to import const like `IS_DEV` from `@extension/env` like:
|
||||
```ts
|
||||
import { IS_DEV } from '@extension/env';
|
||||
```
|
||||
For more look [ENV CONST](lib/const.ts)
|
||||
9
packages/env/index.mts
vendored
Normal file
9
packages/env/index.mts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import { baseEnv, dynamicEnvValues } from './lib/index.js';
|
||||
import type { EnvType } from './lib/types.js';
|
||||
|
||||
export * from './lib/index.js';
|
||||
|
||||
export default {
|
||||
...baseEnv,
|
||||
...dynamicEnvValues,
|
||||
} as EnvType;
|
||||
10
packages/env/lib/config.ts
vendored
Normal file
10
packages/env/lib/config.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { config } from '@dotenvx/dotenvx';
|
||||
|
||||
export const baseEnv =
|
||||
config({
|
||||
path: `${import.meta.dirname}/../../../../.env`,
|
||||
}).parsed ?? {};
|
||||
|
||||
export const dynamicEnvValues = {
|
||||
CEB_NODE_ENV: baseEnv.CEB_DEV === 'true' ? 'development' : 'production',
|
||||
} as const;
|
||||
4
packages/env/lib/const.ts
vendored
Normal file
4
packages/env/lib/const.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export const IS_DEV = process.env['CLI_CEB_DEV'] === 'true';
|
||||
export const IS_PROD = !IS_DEV;
|
||||
export const IS_FIREFOX = process.env['CLI_CEB_FIREFOX'] === 'true';
|
||||
export const IS_CI = process.env['CEB_CI'] === 'true';
|
||||
2
packages/env/lib/index.ts
vendored
Normal file
2
packages/env/lib/index.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './config.js';
|
||||
export * from './const.js';
|
||||
13
packages/env/lib/types.ts
vendored
Normal file
13
packages/env/lib/types.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { dynamicEnvValues } from './index.js';
|
||||
|
||||
interface ICebEnv {
|
||||
readonly CEB_EXAMPLE: string;
|
||||
readonly CEB_DEV_LOCALE: string;
|
||||
}
|
||||
|
||||
interface ICebCliEnv {
|
||||
readonly CLI_CEB_DEV: string;
|
||||
readonly CLI_CEB_FIREFOX: string;
|
||||
}
|
||||
|
||||
export type EnvType = ICebEnv & ICebCliEnv & typeof dynamicEnvValues;
|
||||
30
packages/env/package.json
vendored
Normal file
30
packages/env/package.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@extension/env",
|
||||
"version": "0.5.0",
|
||||
"description": "chrome extension - environment variables",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"types": "index.mts",
|
||||
"main": "dist/index.mjs",
|
||||
"scripts": {
|
||||
"clean:bundle": "rimraf dist",
|
||||
"clean:node_modules": "pnpx rimraf node_modules",
|
||||
"clean:turbo": "rimraf .turbo",
|
||||
"clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo",
|
||||
"ready": "tsc -b",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"format": "prettier . --write --ignore-path ../../.prettierignore",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dotenvx/dotenvx": "^1.44.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@extension/tsconfig": "workspace:*"
|
||||
}
|
||||
}
|
||||
8
packages/env/tsconfig.json
vendored
Normal file
8
packages/env/tsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@extension/tsconfig/module",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["index.mts", "lib"]
|
||||
}
|
||||
BIN
packages/env/use-env-example.png
vendored
Normal file
BIN
packages/env/use-env-example.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
1
packages/hmr/index.mts
Normal file
1
packages/hmr/index.mts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/plugins/index.js';
|
||||
6
packages/hmr/lib/consts.ts
Normal file
6
packages/hmr/lib/consts.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const LOCAL_RELOAD_SOCKET_PORT = 8081;
|
||||
export const LOCAL_RELOAD_SOCKET_URL = `ws://localhost:${LOCAL_RELOAD_SOCKET_PORT}`;
|
||||
|
||||
export const DO_UPDATE = 'do_update';
|
||||
export const DONE_UPDATE = 'done_update';
|
||||
export const BUILD_COMPLETE = 'build_complete';
|
||||
17
packages/hmr/lib/initializers/init-client.ts
Normal file
17
packages/hmr/lib/initializers/init-client.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { DO_UPDATE, DONE_UPDATE, LOCAL_RELOAD_SOCKET_URL } from '../consts.js';
|
||||
import MessageInterpreter from '../interpreter/index.js';
|
||||
|
||||
export default ({ id, onUpdate }: { id: string; onUpdate: () => void }) => {
|
||||
const ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.addEventListener('message', event => {
|
||||
const message = MessageInterpreter.receive(String(event.data));
|
||||
|
||||
if (message.type === DO_UPDATE && message.id === id) {
|
||||
onUpdate();
|
||||
ws.send(MessageInterpreter.send({ type: DONE_UPDATE }));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
53
packages/hmr/lib/initializers/init-reload-server.ts
Normal file
53
packages/hmr/lib/initializers/init-reload-server.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
BUILD_COMPLETE,
|
||||
DO_UPDATE,
|
||||
DONE_UPDATE,
|
||||
LOCAL_RELOAD_SOCKET_PORT,
|
||||
LOCAL_RELOAD_SOCKET_URL,
|
||||
} from '../consts.js';
|
||||
import MessageInterpreter from '../interpreter/index.js';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import type { WebSocket } from 'ws';
|
||||
|
||||
const clientsThatNeedToUpdate: Set<WebSocket> = new Set();
|
||||
|
||||
(() => {
|
||||
const wss = new WebSocketServer({ port: LOCAL_RELOAD_SOCKET_PORT });
|
||||
|
||||
wss.on('listening', () => {
|
||||
console.log(`[HMR] Server listening at ${LOCAL_RELOAD_SOCKET_URL}`);
|
||||
});
|
||||
|
||||
wss.on('connection', ws => {
|
||||
clientsThatNeedToUpdate.add(ws);
|
||||
|
||||
ws.addEventListener('close', () => {
|
||||
clientsThatNeedToUpdate.delete(ws);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', event => {
|
||||
if (typeof event.data !== 'string') return;
|
||||
|
||||
const message = MessageInterpreter.receive(event.data);
|
||||
|
||||
if (message.type === DONE_UPDATE) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
if (message.type === BUILD_COMPLETE) {
|
||||
clientsThatNeedToUpdate.forEach((ws: WebSocket) =>
|
||||
ws.send(MessageInterpreter.send({ type: DO_UPDATE, id: message.id })),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('error', (error: Error & { code: string }) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
console.info(`[HMR] Server already running at ${LOCAL_RELOAD_SOCKET_URL}, skipping reload server initialization`);
|
||||
} else {
|
||||
console.error(`[HMR] Failed to start server at ${LOCAL_RELOAD_SOCKET_URL}`);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
})();
|
||||
33
packages/hmr/lib/injections/refresh.ts
Normal file
33
packages/hmr/lib/injections/refresh.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import initClient from '../initializers/init-client.js';
|
||||
|
||||
(() => {
|
||||
let pendingReload = false;
|
||||
|
||||
initClient({
|
||||
// @ts-expect-error That's because of the dynamic code loading
|
||||
id: __HMR_ID,
|
||||
onUpdate: () => {
|
||||
// disable reload when tab is hidden
|
||||
if (document.hidden) {
|
||||
pendingReload = true;
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
},
|
||||
});
|
||||
|
||||
// reload
|
||||
const reload = (): void => {
|
||||
pendingReload = false;
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// reload when tab is visible
|
||||
const reloadWhenTabIsVisible = (): void => {
|
||||
if (!document.hidden && pendingReload) {
|
||||
reload();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', reloadWhenTabIsVisible);
|
||||
})();
|
||||
13
packages/hmr/lib/injections/reload.ts
Normal file
13
packages/hmr/lib/injections/reload.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import initClient from '../initializers/init-client.js';
|
||||
|
||||
(() => {
|
||||
const reload = () => {
|
||||
chrome.runtime.reload();
|
||||
};
|
||||
|
||||
initClient({
|
||||
// @ts-expect-error That's because of the dynamic code loading
|
||||
id: __HMR_ID,
|
||||
onUpdate: reload,
|
||||
});
|
||||
})();
|
||||
6
packages/hmr/lib/interpreter/index.ts
Normal file
6
packages/hmr/lib/interpreter/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { WebSocketMessageType } from '../types.js';
|
||||
|
||||
export default {
|
||||
send: (message: WebSocketMessageType): string => JSON.stringify(message),
|
||||
receive: (serializedMessage: string): WebSocketMessageType => JSON.parse(serializedMessage),
|
||||
};
|
||||
3
packages/hmr/lib/plugins/index.ts
Normal file
3
packages/hmr/lib/plugins/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './watch-rebuild-plugin.js';
|
||||
export * from './watch-public-plugin.js';
|
||||
export * from './make-entry-point-plugin.js';
|
||||
73
packages/hmr/lib/plugins/make-entry-point-plugin.ts
Normal file
73
packages/hmr/lib/plugins/make-entry-point-plugin.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { IS_FIREFOX } from '@extension/env';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { basename, resolve, sep } from 'node:path';
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
/**
|
||||
* Extract content directory from output directory for Firefox
|
||||
* @param outputDir
|
||||
*/
|
||||
const extractContentDir = (outputDir: string) => {
|
||||
const parts = outputDir.split(sep);
|
||||
const distIndex = parts.indexOf('dist');
|
||||
|
||||
if (distIndex !== -1 && distIndex < parts.length - 1) {
|
||||
return parts.slice(distIndex + 1);
|
||||
}
|
||||
|
||||
throw new Error('Output directory does not contain "dist"');
|
||||
};
|
||||
|
||||
const safeWriteFileSync = (path: string, data: string) => {
|
||||
const folder = path.split(sep).slice(0, -1).join(sep);
|
||||
|
||||
if (!existsSync(folder)) {
|
||||
mkdirSync(folder, { recursive: true });
|
||||
}
|
||||
writeFileSync(path, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make an entry point file for content script cache busting
|
||||
*/
|
||||
export const makeEntryPointPlugin = (): PluginOption => ({
|
||||
name: 'make-entry-point-plugin',
|
||||
generateBundle(options, bundle) {
|
||||
const outputDir = options.dir;
|
||||
|
||||
if (!outputDir) {
|
||||
throw new Error('Output directory not found');
|
||||
}
|
||||
|
||||
for (const module of Object.values(bundle)) {
|
||||
const fileName = module.fileName;
|
||||
const newFileName = fileName.replace('.js', '_dev.js');
|
||||
|
||||
switch (module.type) {
|
||||
case 'asset':
|
||||
if (fileName.endsWith('.map')) {
|
||||
const originalFileName = fileName.replace('.map', '');
|
||||
const replacedSource = String(module.source).replaceAll(originalFileName, newFileName);
|
||||
|
||||
module.source = '';
|
||||
safeWriteFileSync(resolve(outputDir, newFileName), replacedSource);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'chunk': {
|
||||
safeWriteFileSync(resolve(outputDir, newFileName), module.code);
|
||||
const newFileNameBase = basename(newFileName);
|
||||
|
||||
if (IS_FIREFOX) {
|
||||
const contentDirectory = extractContentDir(outputDir);
|
||||
module.code = `import(browser.runtime.getURL("${contentDirectory}/${newFileNameBase}"));`;
|
||||
} else {
|
||||
module.code = `import('./${newFileNameBase}');`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
13
packages/hmr/lib/plugins/watch-public-plugin.ts
Normal file
13
packages/hmr/lib/plugins/watch-public-plugin.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import fg from 'fast-glob';
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
export const watchPublicPlugin = (): PluginOption => ({
|
||||
name: 'watch-public-plugin',
|
||||
async buildStart() {
|
||||
const files = await fg(['public/**/*']);
|
||||
|
||||
for (const file of files) {
|
||||
this.addWatchFile(file);
|
||||
}
|
||||
},
|
||||
});
|
||||
68
packages/hmr/lib/plugins/watch-rebuild-plugin.ts
Normal file
68
packages/hmr/lib/plugins/watch-rebuild-plugin.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { BUILD_COMPLETE, LOCAL_RELOAD_SOCKET_URL } from '../consts.js';
|
||||
import MessageInterpreter from '../interpreter/index.js';
|
||||
import { WebSocket } from 'ws';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import type { PluginConfigType } from '../types.js';
|
||||
import type { PluginOption } from 'vite';
|
||||
|
||||
const injectionsPath = resolve(import.meta.dirname, '..', 'injections');
|
||||
|
||||
const refreshCode = readFileSync(resolve(injectionsPath, 'refresh.js'), 'utf-8');
|
||||
const reloadCode = readFileSync(resolve(injectionsPath, 'reload.js'), 'utf-8');
|
||||
|
||||
export const watchRebuildPlugin = (config: PluginConfigType): PluginOption => {
|
||||
const { refresh, reload, id: _id, onStart } = config;
|
||||
const hmrCode = (refresh ? refreshCode : '') + (reload ? reloadCode : '');
|
||||
|
||||
let ws: WebSocket | null = null;
|
||||
|
||||
const id = _id ?? Math.random().toString(36);
|
||||
let reconnectTries = 0;
|
||||
|
||||
const initializeWebSocket = () => {
|
||||
ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log(`[HMR] Connected to dev-server at ${LOCAL_RELOAD_SOCKET_URL}`);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
console.error(`[HMR] Failed to connect server at ${LOCAL_RELOAD_SOCKET_URL}`);
|
||||
console.warn('Retrying in 3 seconds...');
|
||||
ws = null;
|
||||
|
||||
if (reconnectTries <= 2) {
|
||||
setTimeout(() => {
|
||||
reconnectTries++;
|
||||
initializeWebSocket();
|
||||
}, 3_000);
|
||||
} else {
|
||||
console.error(`[HMR] Cannot establish connection to server at ${LOCAL_RELOAD_SOCKET_URL}`);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'watch-rebuild',
|
||||
closeBundle() {
|
||||
onStart?.();
|
||||
if (!ws) {
|
||||
initializeWebSocket();
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* When the build is complete, send a message to the reload server.
|
||||
* The reload server will send a message to the client to reload or refresh the extension.
|
||||
*/
|
||||
ws.send(MessageInterpreter.send({ type: BUILD_COMPLETE, id }));
|
||||
},
|
||||
generateBundle(_options, bundle) {
|
||||
for (const module of Object.values(bundle)) {
|
||||
if (module.type === 'chunk') {
|
||||
module.code = `(function() {let __HMR_ID = "${id}";\n` + hmrCode + '\n' + '})();' + '\n' + module.code;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
18
packages/hmr/lib/types.ts
Normal file
18
packages/hmr/lib/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { BUILD_COMPLETE, DO_UPDATE, DONE_UPDATE } from './consts.js';
|
||||
|
||||
type UpdateRequestMessageType = {
|
||||
type: typeof DO_UPDATE;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type UpdateCompleteMessageType = { type: typeof DONE_UPDATE };
|
||||
type BuildCompletionMessageType = { type: typeof BUILD_COMPLETE; id: string };
|
||||
|
||||
export type WebSocketMessageType = UpdateCompleteMessageType | UpdateRequestMessageType | BuildCompletionMessageType;
|
||||
|
||||
export type PluginConfigType = {
|
||||
onStart?: () => void;
|
||||
reload?: boolean;
|
||||
refresh?: boolean;
|
||||
id?: string;
|
||||
};
|
||||
36
packages/hmr/package.json
Normal file
36
packages/hmr/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@extension/hmr",
|
||||
"version": "0.5.0",
|
||||
"description": "chrome extension - hot module reload/refresh",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"sideEffects": true,
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"types": "index.mts",
|
||||
"main": "dist/index.mjs",
|
||||
"scripts": {
|
||||
"clean:bundle": "rimraf dist && pnpx rimraf build",
|
||||
"clean:node_modules": "pnpx rimraf node_modules",
|
||||
"clean:turbo": "rimraf .turbo",
|
||||
"clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo",
|
||||
"ready": "tsc -b && rollup --config dist/rollup.config.js",
|
||||
"dev": "tsx lib/initializers/init-reload-server.ts",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"format": "prettier . --write --ignore-path ../../.prettierignore",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@extension/env": "workspace:*",
|
||||
"@extension/tsconfig": "workspace:*",
|
||||
"@rollup/plugin-sucrase": "^5.0.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.25.4",
|
||||
"esm": "^3.2.25",
|
||||
"rollup": "^4.41.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
}
|
||||
29
packages/hmr/rollup.config.ts
Normal file
29
packages/hmr/rollup.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import sucrase from '@rollup/plugin-sucrase';
|
||||
import type { Plugin, RollupOptions } from 'rollup';
|
||||
|
||||
const plugins = [
|
||||
// @ts-expect-error Because of the lack of calling signature
|
||||
sucrase({
|
||||
exclude: ['node_modules/**'],
|
||||
transforms: ['typescript'],
|
||||
}),
|
||||
] satisfies Plugin[];
|
||||
|
||||
export default [
|
||||
{
|
||||
plugins,
|
||||
input: 'lib/injections/reload.ts',
|
||||
output: {
|
||||
format: 'esm',
|
||||
file: 'dist/lib/injections/reload.js',
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins,
|
||||
input: 'lib/injections/refresh.ts',
|
||||
output: {
|
||||
format: 'esm',
|
||||
file: 'dist/lib/injections/refresh.js',
|
||||
},
|
||||
},
|
||||
] satisfies RollupOptions[];
|
||||
9
packages/hmr/tsconfig.json
Normal file
9
packages/hmr/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@extension/tsconfig/module",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"types": ["chrome", "node"]
|
||||
},
|
||||
"include": ["index.mts", "rollup.config.ts", "lib"]
|
||||
}
|
||||
1
packages/i18n/.gitignore
vendored
Normal file
1
packages/i18n/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
lib/i18n.ts
|
||||
173
packages/i18n/README.md
Normal file
173
packages/i18n/README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# I18n Package
|
||||
|
||||
This package provides a set of tools to help you internationalize your Chrome Extension.
|
||||
|
||||
https://developer.chrome.com/docs/extensions/reference/api/i18n
|
||||
|
||||
## Installation
|
||||
|
||||
If you want to use the i18n translation function in each pages, you need to add the following to the package.json file.
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@extension/i18n": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run the following command to install the package.
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Manage translations
|
||||
|
||||
You can manage translations in the `locales` directory.
|
||||
|
||||
`locales/en/messages.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"helloWorld": {
|
||||
"message": "Hello, World!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`locales/ko/messages.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"helloWorld": {
|
||||
"message": "안녕하세요, 여러분!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Add a new language
|
||||
|
||||
Create folder inside `locales` with name from [languages](https://developer.chrome.com/docs/extensions/reference/api/i18n?hl=pl#support_multiple_languages), which need include `message.json` file.
|
||||
|
||||
## Usage
|
||||
|
||||
### Translation function
|
||||
|
||||
Just import the `t` function and use it to translate the key.
|
||||
|
||||
```typescript
|
||||
import { t } from '@extension/i18n';
|
||||
|
||||
console.log(t('loading')); // Loading...
|
||||
```
|
||||
|
||||
```typescript jsx
|
||||
import { t } from '@extension/i18n';
|
||||
|
||||
const Component = () => {
|
||||
return (
|
||||
<button>
|
||||
{t('toggleTheme')} // Toggle Theme
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Placeholders
|
||||
|
||||
If you want to use placeholders, you can use the following format.
|
||||
|
||||
> For more information, see the [Message Placeholders](https://developer.chrome.com/docs/extensions/how-to/ui/localization-message-formats#placeholders) section.
|
||||
|
||||
`locales/en/messages.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"greeting": {
|
||||
"description": "Greeting message",
|
||||
"message": "Hello, My name is $NAME$",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "John Doe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hello": {
|
||||
"description": "Placeholder example",
|
||||
"message": "Hello $1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`locales/ko/messages.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"greeting": {
|
||||
"description": "인사 메시지",
|
||||
"message": "안녕하세요, 제 이름은 $NAME$입니다.",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "서종학"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hello": {
|
||||
"description": "Placeholder 예시",
|
||||
"message": "안녕 $1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you want to replace the placeholder, you can pass the value as the second argument.
|
||||
|
||||
Function `t` has exactly the same interface as the `chrome.i18n.getMessage` function.
|
||||
|
||||
```typescript
|
||||
import { t } from '@extension/i18n';
|
||||
|
||||
console.log(t('greeting', 'John Doe')); // Hello, My name is John Doe
|
||||
console.log(t('greeting', ['John Doe'])); // Hello, My name is John Doe
|
||||
|
||||
console.log(t('hello')); // Hello
|
||||
console.log(t('hello', 'World')); // Hello World
|
||||
console.log(t('hello', ['World'])); // Hello World
|
||||
```
|
||||
|
||||
### Locale setting on development
|
||||
|
||||
If you want to enforce displaying specific language, you need to set `CEB_DEV_LOCALE` in `.env` file (work only for development).
|
||||
|
||||
### Type Safety
|
||||
|
||||
When you forget to add a key to all language's `messages.json` files, you will get a Typescript error.
|
||||
|
||||
`locales/en/messages.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"hello": {
|
||||
"message": "Hello World!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`locales/ko/messages.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"helloWorld": {
|
||||
"message": "안녕하세요, 여러분!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { t } from '@extension/i18n';
|
||||
|
||||
// Error: TS2345: Argument of type "hello" is not assignable to parameter of type
|
||||
console.log(t('hello'));
|
||||
```
|
||||
1
packages/i18n/index.mts
Normal file
1
packages/i18n/index.mts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/index.js';
|
||||
62
packages/i18n/lib/consts.ts
Normal file
62
packages/i18n/lib/consts.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @url https://developer.chrome.com/docs/extensions/reference/api/i18n#support_multiple_languages
|
||||
*/
|
||||
export const SUPPORTED_LANGUAGES = {
|
||||
ar: 'Arabic',
|
||||
am: 'Amharic',
|
||||
bg: 'Bulgarian',
|
||||
bn: 'Bengali',
|
||||
ca: 'Catalan',
|
||||
cs: 'Czech',
|
||||
da: 'Danish',
|
||||
de: 'German',
|
||||
el: 'Greek',
|
||||
en: 'English',
|
||||
en_AU: 'English (Australia)',
|
||||
en_GB: 'English (Great Britain)',
|
||||
en_US: 'English (USA)',
|
||||
es: 'Spanish',
|
||||
es_419: 'Spanish (Latin America and Caribbean)',
|
||||
et: 'Estonian',
|
||||
fa: 'Persian',
|
||||
fi: 'Finnish',
|
||||
fil: 'Filipino',
|
||||
fr: 'French',
|
||||
gu: 'Gujarati',
|
||||
he: 'Hebrew',
|
||||
hi: 'Hindi',
|
||||
hr: 'Croatian',
|
||||
hu: 'Hungarian',
|
||||
id: 'Indonesian',
|
||||
it: 'Italian',
|
||||
ja: 'Japanese',
|
||||
kn: 'Kannada',
|
||||
ko: 'Korean',
|
||||
lt: 'Lithuanian',
|
||||
lv: 'Latvian',
|
||||
ml: 'Malayalam',
|
||||
mr: 'Marathi',
|
||||
ms: 'Malay',
|
||||
nl: 'Dutch',
|
||||
no: 'Norwegian',
|
||||
pl: 'Polish',
|
||||
pt_BR: 'Portuguese (Brazil)',
|
||||
pt_PT: 'Portuguese (Portugal)',
|
||||
ro: 'Romanian',
|
||||
ru: 'Russian',
|
||||
sk: 'Slovak',
|
||||
sl: 'Slovenian',
|
||||
sr: 'Serbian',
|
||||
sv: 'Swedish',
|
||||
sw: 'Swahili',
|
||||
ta: 'Tamil',
|
||||
te: 'Telugu',
|
||||
th: 'Thai',
|
||||
tr: 'Turkish',
|
||||
uk: 'Ukrainian',
|
||||
vi: 'Vietnamese',
|
||||
zh_CN: 'Chinese (China)',
|
||||
zh_TW: 'Chinese (Taiwan)',
|
||||
} as const;
|
||||
|
||||
export const I18N_FILE_PATH = './lib/i18n.ts';
|
||||
34
packages/i18n/lib/i18n-dev.ts
Normal file
34
packages/i18n/lib/i18n-dev.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// IT WILL BE ADJUSTED TO YOUR LANGUAGE DURING BUILD TIME, DON'T MOVE BELOW IMPORT TO OTHER LINE
|
||||
import localeJSON from '../locales/en/messages.json' with { type: 'json' };
|
||||
import type { I18nValueType, LocalesJSONType } from './types.js';
|
||||
|
||||
const translate = (key: keyof LocalesJSONType, substitutions?: string | string[]) => {
|
||||
const localeValues = localeJSON[key] as I18nValueType;
|
||||
let message = localeValues.message;
|
||||
/**
|
||||
* This is a placeholder replacement logic. But it's not perfect.
|
||||
* It just imitates the behavior of the Chrome extension i18n API.
|
||||
* Please check the official document for more information And double-check the behavior on production build.
|
||||
*
|
||||
* @url https://developer.chrome.com/docs/extensions/how-to/ui/localization-message-formats#placeholders
|
||||
*/
|
||||
if (localeValues.placeholders) {
|
||||
Object.entries(localeValues.placeholders).forEach(([key, { content }]) => {
|
||||
if (content) {
|
||||
message = message.replace(new RegExp(`\\$${key}\\$`, 'gi'), content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!substitutions) {
|
||||
return message;
|
||||
} else if (Array.isArray(substitutions)) {
|
||||
return substitutions.reduce((acc, cur, idx) => acc.replace(`$${idx++}`, cur), message);
|
||||
}
|
||||
|
||||
return message.replace(/\$(\d+)/, substitutions);
|
||||
};
|
||||
|
||||
const removePlaceholder = (message: string) => message.replace(/\$\d+/g, '');
|
||||
|
||||
export const t = (...args: Parameters<typeof translate>) => removePlaceholder(translate(...args));
|
||||
3
packages/i18n/lib/i18n-prod.ts
Normal file
3
packages/i18n/lib/i18n-prod.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { MessageKeyType } from './types.js';
|
||||
|
||||
export const t = (key: MessageKeyType, substitutions?: string | string[]) => chrome.i18n.getMessage(key, substitutions);
|
||||
4
packages/i18n/lib/index.ts
Normal file
4
packages/i18n/lib/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { t as t_dev_or_prod } from './i18n.js';
|
||||
import type { t as t_dev } from './i18n-dev.js';
|
||||
|
||||
export const t = t_dev_or_prod as unknown as typeof t_dev;
|
||||
22
packages/i18n/lib/prepare-build.ts
Normal file
22
packages/i18n/lib/prepare-build.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import setRelatedLocaleImports from './set-related-locale-import.js';
|
||||
import { IS_DEV } from '@extension/env';
|
||||
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
(() => {
|
||||
const i18nPath = IS_DEV ? 'lib/i18n-dev.ts' : 'lib/i18n-prod.ts';
|
||||
cpSync(i18nPath, resolve('lib', 'i18n.ts'));
|
||||
|
||||
const outDir = resolve(import.meta.dirname, '..', '..', '..', '..', 'dist');
|
||||
if (!existsSync(outDir)) {
|
||||
mkdirSync(outDir);
|
||||
}
|
||||
|
||||
const localePath = resolve(outDir, '_locales');
|
||||
cpSync(resolve('locales'), localePath, { recursive: true });
|
||||
|
||||
if (IS_DEV) {
|
||||
setRelatedLocaleImports();
|
||||
}
|
||||
console.log('I18n build complete');
|
||||
})();
|
||||
38
packages/i18n/lib/set-related-locale-import.ts
Normal file
38
packages/i18n/lib/set-related-locale-import.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { I18N_FILE_PATH } from './consts.js';
|
||||
import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import type { SupportedLanguagesKeysType, SupportedLanguagesWithoutRegionKeysType } from './types.js';
|
||||
|
||||
export default () => {
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale.replace('-', '_') as SupportedLanguagesKeysType;
|
||||
const localeWithoutRegion = locale.split('_')[0] as SupportedLanguagesWithoutRegionKeysType;
|
||||
|
||||
const localesDir = resolve(import.meta.dirname, '..', '..', 'locales');
|
||||
const readLocalesFolder = readdirSync(localesDir);
|
||||
|
||||
const implementedLocales = readLocalesFolder.map(innerDir => {
|
||||
if (lstatSync(resolve(localesDir, innerDir)).isDirectory()) {
|
||||
return innerDir;
|
||||
}
|
||||
return;
|
||||
});
|
||||
|
||||
const i18nFileSplitContent = readFileSync(I18N_FILE_PATH, 'utf-8').split('\n');
|
||||
|
||||
if (process.env['CEB_DEV_LOCALE']) {
|
||||
i18nFileSplitContent[1] = `import localeJSON from '../locales/${process.env['CEB_DEV_LOCALE']}/messages.json' with { type: 'json' };`;
|
||||
} else {
|
||||
if (implementedLocales.includes(locale)) {
|
||||
i18nFileSplitContent[1] = `import localeJSON from '../locales/${locale}/messages.json' with { type: 'json' };`;
|
||||
} else if (implementedLocales.includes(localeWithoutRegion)) {
|
||||
i18nFileSplitContent[1] = `import localeJSON from '../locales/${localeWithoutRegion}/messages.json' with { type: 'json' };`;
|
||||
} else {
|
||||
i18nFileSplitContent[1] = `import localeJSON from '../locales/en/messages.json' with { type: 'json' };`;
|
||||
}
|
||||
}
|
||||
|
||||
// Join lines back together
|
||||
const updatedI18nFile = i18nFileSplitContent.join('\n');
|
||||
|
||||
writeFileSync(I18N_FILE_PATH, updatedI18nFile, 'utf-8');
|
||||
};
|
||||
12
packages/i18n/lib/types.ts
Normal file
12
packages/i18n/lib/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import enMessage from '../locales/en/messages.json' with { type: 'json' };
|
||||
import type { SUPPORTED_LANGUAGES } from './consts.js';
|
||||
|
||||
export type SupportedLanguagesKeysType = keyof typeof SUPPORTED_LANGUAGES;
|
||||
export type SupportedLanguagesWithoutRegionKeysType = Exclude<SupportedLanguagesKeysType, `${string}_${string}`>;
|
||||
export type I18nValueType = {
|
||||
message: string;
|
||||
placeholders?: Record<string, { content?: string; example?: string }>;
|
||||
};
|
||||
|
||||
export type MessageKeyType = keyof typeof enMessage;
|
||||
export type LocalesJSONType = typeof enMessage;
|
||||
45
packages/i18n/locales/en/messages.json
Normal file
45
packages/i18n/locales/en/messages.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"extensionDescription": {
|
||||
"description": "Extension description",
|
||||
"message": "Chrome extension boilerplate developed with Vite, React and Typescript"
|
||||
},
|
||||
"extensionName": {
|
||||
"description": "Extension name",
|
||||
"message": "Chrome extension boilerplate"
|
||||
},
|
||||
"toggleTheme": {
|
||||
"message": "Toggle theme"
|
||||
},
|
||||
"injectButton": {
|
||||
"message": "Click to inject Content Scripts"
|
||||
},
|
||||
"greeting": {
|
||||
"description": "Greeting message",
|
||||
"message": "Hello, My name is $NAME$",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "John Doe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hello": {
|
||||
"description": "Placeholder example",
|
||||
"message": "Hello $1"
|
||||
},
|
||||
"displayErrorInfo": {
|
||||
"message": "Error occur"
|
||||
},
|
||||
"displayErrorDescription": {
|
||||
"message": "Sorry, something went wrong while loading the page."
|
||||
},
|
||||
"displayErrorDetailsInfo": {
|
||||
"message": "Error details:"
|
||||
},
|
||||
"displayErrorUnknownErrorInfo": {
|
||||
"message": "Unknown error"
|
||||
},
|
||||
"displayErrorReset": {
|
||||
"message": "Run Again"
|
||||
}
|
||||
}
|
||||
45
packages/i18n/locales/ko/messages.json
Normal file
45
packages/i18n/locales/ko/messages.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"extensionDescription": {
|
||||
"description": "Extension description",
|
||||
"message": "React, Typescript, Vite를 사용한 크롬 익스텐션 보일러플레이트입니다."
|
||||
},
|
||||
"extensionName": {
|
||||
"description": "Extension name",
|
||||
"message": "크롬 익스텐션 보일러플레이트"
|
||||
},
|
||||
"toggleTheme": {
|
||||
"message": "테마 변경"
|
||||
},
|
||||
"injectButton": {
|
||||
"message": "콘텐츠 스크립트를 삽입하려면 클릭하세요."
|
||||
},
|
||||
"greeting": {
|
||||
"description": "인사 메시지",
|
||||
"message": "안녕하세요, 제 이름은 $NAME$입니다.",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "홍길동"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hello": {
|
||||
"description": "Placeholder 예시",
|
||||
"message": "안녕하세요, $1님."
|
||||
},
|
||||
"displayErrorInfo": {
|
||||
"message": "오류 발생"
|
||||
},
|
||||
"displayErrorDescription": {
|
||||
"message": "죄송합니다. 페이지를 로드하는 중 문제가 발생했습니다."
|
||||
},
|
||||
"displayErrorDetailsInfo": {
|
||||
"message": "오류 세부 정보:"
|
||||
},
|
||||
"displayErrorUnknownErrorInfo": {
|
||||
"message": "알 수 없는 오류"
|
||||
},
|
||||
"displayErrorReset": {
|
||||
"message": "다시 실행합니다."
|
||||
}
|
||||
}
|
||||
30
packages/i18n/package.json
Normal file
30
packages/i18n/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@extension/i18n",
|
||||
"version": "0.5.0",
|
||||
"description": "chrome extension - internationalization",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/**"
|
||||
],
|
||||
"main": "dist/index.mjs",
|
||||
"types": "index.mts",
|
||||
"scripts": {
|
||||
"clean:bundle": "rimraf dist",
|
||||
"clean:node_modules": "pnpx rimraf node_modules",
|
||||
"clean:turbo": "rimraf .turbo",
|
||||
"clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo",
|
||||
"ready": "tsc -b prepare-build.tsconfig.json && node --env-file=../../.env dist/lib/prepare-build.js && tsc -b",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"format": "prettier . --write --ignore-path ../../.prettierignore",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@extension/env": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@extension/tsconfig": "workspace:*"
|
||||
}
|
||||
}
|
||||
4
packages/i18n/prepare-build.tsconfig.json
Normal file
4
packages/i18n/prepare-build.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["lib/prepare-build.ts"]
|
||||
}
|
||||
8
packages/i18n/tsconfig.json
Normal file
8
packages/i18n/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@extension/tsconfig/module",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["index.mts", "lib"]
|
||||
}
|
||||
64
packages/module-manager/README.md
Normal file
64
packages/module-manager/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Module manager
|
||||
|
||||
Module manager is a tool to manage modules in a project. It can be used to delete or recover some pages.
|
||||
|
||||
## Usage
|
||||
|
||||
### On Root
|
||||
|
||||
```bash
|
||||
$ pnpm module-manager
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```bash
|
||||
pnpm module-manager -d popup
|
||||
```
|
||||
|
||||
### On Module
|
||||
|
||||
```bash
|
||||
$ pnpm start
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```bash
|
||||
pnpm start -d popup
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> For complete info about CLI support run:
|
||||
> `pnpm module-manager --help`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you want to remove all tests, and something else with one command, you need to set `tests` as first argument like:
|
||||
> `pnpm module-manager -d tests popup ...`
|
||||
|
||||
### Choose a tool
|
||||
|
||||
```
|
||||
? Choose a tool (Use arrow keys)
|
||||
❯ Delete Feature
|
||||
Recover Feature
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
### Delete Feature
|
||||
|
||||
When you select an unused module, Module Manager compresses the contents of that folder, takes a snapshot of it, and removes it.
|
||||
|
||||
It also automatically removes anything that needs to be cleared from the manifest.
|
||||
|
||||
### Recover Feature
|
||||
|
||||
When you select a module that has been deleted, The Module Manager will recover that module from the snapshot and add it back to the manifest.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> SAVE ALL FILES FROM /ARCHIVE, WITHOUT THESE FILES' RECOVERING PROCESS WON'T WORK
|
||||
|
||||
> [!IMPORTANT]
|
||||
> IF YOU DECIDE TO REMOVE ONE OF CONTENT SCRIPTs AFTER EDIT, E.G YOU REMOVE OR ADD ANY MATCHES,
|
||||
> REMEMBER TO EDIT ALSO [MODULE_CONFIG](./lib/const.ts) TO BE ABLE TO RECOVER IT EASILY IN THE FUTURE.
|
||||
3
packages/module-manager/index.mts
Normal file
3
packages/module-manager/index.mts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { startManager } from './lib/index.js';
|
||||
|
||||
startManager();
|
||||
51
packages/module-manager/lib/base/cli-args-processor.ts
Normal file
51
packages/module-manager/lib/base/cli-args-processor.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { CLI_OPTIONS, DEFAULT_CHOICES_VALUES, HELP_EXAMPLES } from '../const.js';
|
||||
import { checkCliArgsIsValid } from '../helpers/utils.js';
|
||||
import { excludeValuesFromBaseArray } from '@extension/shared';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import type { ICLIOptions, ModuleNameType } from '../types.js';
|
||||
import type { WritableDeep } from '@extension/shared';
|
||||
|
||||
export const processCLIArgs = (): ICLIOptions | null => {
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.option('delete', CLI_OPTIONS[0])
|
||||
.option('recover', CLI_OPTIONS[1])
|
||||
.option('delete-exclude', CLI_OPTIONS[2])
|
||||
.option('recover-exclude', CLI_OPTIONS[3])
|
||||
.check(argv => checkCliArgsIsValid(argv))
|
||||
.strict()
|
||||
.example(HELP_EXAMPLES as WritableDeep<typeof HELP_EXAMPLES>)
|
||||
.help()
|
||||
.showHelpOnFail(true)
|
||||
.parseSync();
|
||||
|
||||
if (argv.delete) {
|
||||
return {
|
||||
action: 'delete',
|
||||
targets: argv.delete as ModuleNameType[],
|
||||
};
|
||||
}
|
||||
|
||||
if (argv.recover) {
|
||||
return {
|
||||
action: 'recover',
|
||||
targets: argv.recover as ModuleNameType[],
|
||||
};
|
||||
}
|
||||
|
||||
if (argv.deleteExclude) {
|
||||
return {
|
||||
action: 'delete',
|
||||
targets: excludeValuesFromBaseArray(DEFAULT_CHOICES_VALUES, argv.deleteExclude),
|
||||
};
|
||||
}
|
||||
|
||||
if (argv.recoverExclude) {
|
||||
return {
|
||||
action: 'recover',
|
||||
targets: excludeValuesFromBaseArray(DEFAULT_CHOICES_VALUES, argv.recoverExclude),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
2
packages/module-manager/lib/base/index.ts
Normal file
2
packages/module-manager/lib/base/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './cli-args-processor.js';
|
||||
export * from './run-module-manager.js';
|
||||
49
packages/module-manager/lib/base/run-module-manager.ts
Normal file
49
packages/module-manager/lib/base/run-module-manager.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import manifest from '../../../../chrome-extension/manifest.js';
|
||||
import { MANAGER_ACTION_PROMPT_CONFIG } from '../const.js';
|
||||
import { promptSelection } from '../helpers/utils.js';
|
||||
import { deleteFeature, recoverFeature } from '../processing/index.js';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import type { CliActionType, ModuleNameType } from '../types.js';
|
||||
import type { ManifestType } from '@extension/shared';
|
||||
|
||||
const manifestPath = resolve(import.meta.dirname, '..', '..', '..', '..', 'chrome-extension', 'manifest.ts');
|
||||
|
||||
const manifestObject = JSON.parse(JSON.stringify(manifest)) as ManifestType;
|
||||
const manifestString = readFileSync(manifestPath, 'utf-8');
|
||||
|
||||
export const runModuleManager = async (moduleName?: ModuleNameType, action?: CliActionType, isLastLap = true) => {
|
||||
if (!action) {
|
||||
action = (await promptSelection(MANAGER_ACTION_PROMPT_CONFIG)) as Awaited<CliActionType>;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
await deleteFeature(manifestObject, moduleName);
|
||||
break;
|
||||
case 'recover':
|
||||
await recoverFeature(manifestObject, moduleName);
|
||||
}
|
||||
|
||||
const updatedManifest = manifestString
|
||||
.replace(
|
||||
/const manifest = {[\s\S]*?} satisfies/,
|
||||
'const manifest = ' + JSON.stringify(manifestObject, null, 2) + ' satisfies',
|
||||
)
|
||||
.replace(/ {2}"version": "[\s\S]*?",/, ' version: packageJson.version,');
|
||||
|
||||
writeFileSync(manifestPath, updatedManifest);
|
||||
|
||||
if (isLastLap) {
|
||||
execSync('pnpm i', {
|
||||
stdio: 'inherit',
|
||||
cwd: resolve('..', '..'),
|
||||
});
|
||||
|
||||
execSync('pnpm -F chrome-extension lint:fix', {
|
||||
stdio: 'inherit',
|
||||
cwd: resolve('..', '..'),
|
||||
});
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user