diff --git a/.autorc b/.autorc
deleted file mode 100644
index 09b28ca299..0000000000
--- a/.autorc
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "plugins": [
- "git-tag"
- ],
- "owner": "duckduckgo",
- "repo": "content-scope-scripts",
- "name": "dax",
- "email": "dax@duck.co",
- "baseBranch": "releases",
- "onlyPublishWithReleaseLabel": true
-}
diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 0000000000..672eff1778
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1,16 @@
+# Content Scope Scripts - Cursor Rules
+
+## Documentation References
+
+When asked about Content Scope Scripts topics, refer to these documentation files:
+
+- **API Reference**: `injected/docs/api-reference.md`
+- **Feature Development**: `injected/docs/features-guide.md`
+- **Platform Integration and engine support**: `injected/docs/platform-integration.md`
+- **Development Utilities**: `injected/docs/development-utilities.md`
+- **Testing**: `injected/docs/testing-guide.md`
+- **Favicon**: `injected/docs/favicon.md`
+- **Message Bridge**: `injected/docs/message-bridge.md`
+- **Test Pages**: `injected/docs/test-pages-guide.md`
+- **Documentation Index**: `injected/docs/README.md`
+- **High-level Overview**: `injected/README.md`
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index e95bf4133a..0000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,4 +0,0 @@
-build/
-lib/
-Sources/ContentScopeScripts/dist/
-integration-test/extension/contentScope.js
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index e73e2a398d..0000000000
--- a/.eslintrc
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "extends": "standard",
- "root": true,
- "parserOptions": {
- "ecmaVersion": "latest",
- },
- "globals": {
- "$USER_PREFERENCES$": "readonly",
- "$USER_UNPROTECTED_DOMAINS$": "readonly",
- "$CONTENT_SCOPE$": "readonly",
- "$TRACKER_LOOKUP$": "readonly",
- "$BUNDLED_CONFIG$": "readonly"
- },
- "rules": {
- "indent": ["error", 4]
- },
- "env": {
- "webextensions": true,
- "browser": true,
- "jasmine": true
- }
-}
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000000..fa3c6a7855
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,7 @@
+# .git-blame-ignore-revs
+# Moved all packaged to root level https://github.com/duckduckgo/content-scope-scripts/pull/1103
+f16b140c7731f9a66619e426b1f24414abaeb954
+# Switched to a shared DDG ESLint config https://github.com/duckduckgo/content-scope-scripts/pull/1185
+76bab4d80982ddab63265c694ab96c27a0fa5138
+# introduced Prettier https://github.com/duckduckgo/content-scope-scripts/pull/1198
+53818cc0152d4a814d170563145004b65f5d404d
diff --git a/.gitattributes b/.gitattributes
index 6f53f348e8..aeb6ff0bf4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,3 @@
src/locales/** linguist-generated
+src/types/* text eol=lf
+* text=auto eol=lf
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 50a3d03343..e5d5e5f084 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,9 +1,36 @@
version: 2
updates:
- - package-ecosystem: "npm"
- directory: "/"
- schedule:
- interval: "daily"
- target-branch: "main"
- labels:
- - "dependencies"
+ - package-ecosystem: 'npm'
+ directory: '/'
+ schedule:
+ interval: 'weekly'
+ target-branch: 'main'
+ open-pull-requests-limit: 20
+ labels:
+ - 'dependencies'
+ groups:
+ eslint:
+ patterns:
+ - 'eslint*'
+ - '@typescript-eslint*'
+ stylelint:
+ patterns:
+ - 'stylelint*'
+ typescript:
+ patterns:
+ - 'typedoc'
+ - 'typescript'
+ - '@types/*'
+ - '@typescript-eslint*'
+ rollup:
+ patterns:
+ - '@rollup/*'
+ - 'rollup-*'
+ - 'rollup'
+ - package-ecosystem: 'github-actions'
+ directory: '/'
+ schedule:
+ interval: 'weekly'
+ target-branch: 'main'
+ labels:
+ - 'dependencies'
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..9c2cc38ea0
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,29 @@
+**Asana Task/Github Issue:**
+
+## Description
+
+
+
+## Testing Steps
+
+-
+
+## Checklist
+
+
+*Please tick all that apply:*
+
+- [ ] I have tested this change locally
+- [ ] I have tested this change locally in all supported browsers
+- [ ] This change will be visible to users
+- [ ] I have added automated tests that cover this change
+- [ ] I have ensured the change is gated by config
+- [ ] This change was covered by a ship review
+- [ ] This change was covered by a tech design
+- [ ] Any dependent config has been merged
+
diff --git a/.github/scripts/diff-directories.js b/.github/scripts/diff-directories.js
new file mode 100644
index 0000000000..8d1b5408ec
--- /dev/null
+++ b/.github/scripts/diff-directories.js
@@ -0,0 +1,121 @@
+import fs from 'fs';
+import path from 'path';
+
+function readFilesRecursively(directory) {
+ const filenames = fs.readdirSync(directory);
+ const files = {};
+
+ filenames.forEach((filename) => {
+ const filePath = path.join(directory, filename);
+ const fileStats = fs.statSync(filePath);
+
+ if (fileStats.isDirectory()) {
+ const nestedFiles = readFilesRecursively(filePath);
+ for (const [nestedFilePath, nestedFileContent] of Object.entries(nestedFiles)) {
+ files[path.join(filename, nestedFilePath)] = nestedFileContent;
+ }
+ } else {
+ files[filename] = fs.readFileSync(filePath, 'utf-8');
+ }
+ });
+
+ return files;
+}
+
+function upperCaseFirstLetter(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+function displayDiffs(dir1Files, dir2Files) {
+ const rollupGrouping = {};
+ /**
+ * Rolls up multiple files with the same diff into a single entry
+ * @param {string} fileName
+ * @param {string} string
+ * @param {string} [summary]
+ */
+ function add(fileName, string, summary = undefined) {
+ if (summary === undefined) {
+ summary = string;
+ }
+ if (!(summary in rollupGrouping)) {
+ rollupGrouping[summary] = { files: [] };
+ }
+ rollupGrouping[summary].files.push(fileName);
+ rollupGrouping[summary].string = string;
+ }
+ for (const [filePath, fileContent] of Object.entries(dir1Files)) {
+ let diffOut = '';
+ let compareOut;
+ if (filePath in dir2Files) {
+ const fileOut = fileContent;
+ const file2Out = dir2Files[filePath];
+ delete dir2Files[filePath];
+ if (fileOut === file2Out) {
+ continue;
+ } else {
+ compareOut = filePath.split('/')[0];
+ diffOut = `File has changed`;
+ }
+ } else {
+ diffOut = '❌ File only exists in old changeset';
+ compareOut = 'Removed Files';
+ }
+ add(filePath, diffOut, compareOut);
+ }
+
+ for (const filePath of Object.keys(dir2Files)) {
+ add(filePath, '❌ File only exists in new changeset', 'New Files');
+ }
+ const outString = Object.keys(rollupGrouping)
+ .map((key) => {
+ const rollup = rollupGrouping[key];
+ let outString = `
+ `;
+ const title = key;
+ if (rollup.files.length) {
+ for (const file of rollup.files) {
+ outString += `- ${file}\n`;
+ }
+ }
+ outString += '\n\n' + rollup.string;
+ return renderDetails(title, outString);
+ })
+ .join('\n');
+ return outString;
+}
+
+function renderDetails(section, text) {
+ if (section === 'dist') {
+ section = 'apple';
+ }
+ const open = section !== 'integration' ? 'open' : '';
+ return `
+${upperCaseFirstLetter(section)}
+${text}
+ `;
+}
+
+if (process.argv.length !== 4) {
+ console.error('Usage: node diff_directories.js ');
+ process.exit(1);
+}
+
+const dir1 = process.argv[2];
+const dir2 = process.argv[3];
+
+const sections = {};
+function sortFiles(dirFiles, dirName) {
+ for (const [filePath, fileContent] of Object.entries(dirFiles)) {
+ sections[dirName] = sections[dirName] || {};
+ sections[dirName][filePath] = fileContent;
+ }
+}
+
+const buildDir = '/build';
+sortFiles(readFilesRecursively(dir1 + buildDir), 'dir1');
+sortFiles(readFilesRecursively(dir2 + buildDir), 'dir2');
+
+// console.log(Object.keys(files))
+const fileOut = displayDiffs(sections.dir1, sections.dir2);
+console.log(fileOut);
diff --git a/.github/workflows/asana.yml b/.github/workflows/asana.yml
new file mode 100644
index 0000000000..7acddeff06
--- /dev/null
+++ b/.github/workflows/asana.yml
@@ -0,0 +1,25 @@
+name: 'asana sync'
+on:
+ pull_request_review:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - closed
+ - reopened
+ - synchronize
+ - review_requested
+
+jobs:
+ sync:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: duckduckgo/action-asana-sync@v11
+ with:
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ ASANA_WORKSPACE_ID: ${{ secrets.ASANA_WORKSPACE_ID }}
+ ASANA_PROJECT_ID: '1208598406046969'
+ GITHUB_PAT: ${{ secrets.GH_RO_PAT }}
+ USER_MAP: ${{ vars.USER_MAP }}
+ ASSIGN_PR_AUTHOR: 'true'
diff --git a/.github/workflows/auto-respond-pr.yml b/.github/workflows/auto-respond-pr.yml
new file mode 100644
index 0000000000..5049945765
--- /dev/null
+++ b/.github/workflows/auto-respond-pr.yml
@@ -0,0 +1,84 @@
+name: Auto Respond to PR
+
+on:
+ pull_request:
+ types: [opened, synchronize, closed, ready_for_review]
+
+jobs:
+ auto_respond:
+ if: github.actor != 'dependabot[bot]'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout base branch
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.pull_request.base.ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ path: base
+
+ - name: Checkout PR branch
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ path: pr
+ fetch-depth: 0
+
+ - name: Run build script on base branch
+ run: |
+ cd base
+ npm install
+ npm run build
+ cd ..
+
+ - name: Run build script on PR branch
+ run: |
+ cd pr
+ git config --global user.email "dax@duck.com"
+ git config --global user.name "dax"
+ echo ${{ github.event.pull_request.base.ref }}
+ git fetch origin ${{ github.event.pull_request.base.ref }}
+ git rebase -X theirs origin/${{ github.event.pull_request.base.ref }}
+ npm install
+ npm run build
+ cd ..
+
+ - name: Create diff of file outputs
+ run: |
+ node pr/.github/scripts/diff-directories.js base pr > diff.txt
+
+ - name: Find Previous Comment
+ uses: peter-evans/find-comment@v3
+ id: find_comment
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-author: 'github-actions[bot]'
+ body-includes: 'Generated file diff'
+ direction: last
+
+ - name: Create Comment Body
+ uses: actions/github-script@v8
+ id: create_body
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const fs = require('fs');
+ const prNumber = context.issue.number;
+ const diffOut = fs.readFileSync('diff.txt', 'utf8');
+ const commentBody = `
+ ### *[Beta]* Generated file diff
+ *Time updated:* ${new Date().toUTCString()}
+
+ ${diffOut}
+ `;
+ core.setOutput('comment_body', commentBody);
+ core.setOutput('pr_number', prNumber);
+
+ - name: Create, or Update the Comment
+ uses: peter-evans/create-or-update-comment@v4
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find_comment.outputs.comment-id }}
+ body: ${{ steps.create_body.outputs.comment_body }}
+ edit-mode: replace
diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
new file mode 100644
index 0000000000..5452edf2e2
--- /dev/null
+++ b/.github/workflows/build-pr.yml
@@ -0,0 +1,113 @@
+name: PR Build and Release
+
+on:
+ pull_request:
+ types: [opened, synchronize, closed, ready_for_review]
+
+permissions: write-all
+
+jobs:
+ build:
+ if: github.event.action != 'closed'
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+
+ - name: Install dependencies
+ run: npm ci --verbose
+
+ - name: Run build
+ run: npm run build
+
+ - name: Create and push release branch
+ id: create_branch
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git checkout -b pr-releases/pr-${PR_NUMBER}
+ git add -f build
+ git commit -m "Add build folder for PR ${PR_NUMBER}"
+ git push -u origin pr-releases/pr-${PR_NUMBER} --force
+ echo "BRANCH_NAME=pr-releases/pr-${PR_NUMBER}" >> $GITHUB_ENV
+ echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV
+
+ - name: Find Previous Comment
+ uses: peter-evans/find-comment@v3
+ id: find_comment
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-author: 'github-actions[bot]'
+ body-includes: 'Temporary Branch Update'
+ direction: last
+
+ - name: Create Comment Body
+ uses: actions/github-script@v8
+ id: create_body
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const branchName = process.env.BRANCH_NAME;
+ const commitHash = process.env.COMMIT_HASH;
+ const prNumber = context.issue.number;
+ const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`;
+ const branchUrl = `${repoUrl}/tree/${branchName}`;
+ const commitUrl = `${repoUrl}/commit/${commitHash}`;
+
+ // Get the current date
+ const lastUpdatedDate = (new Date()).toLocaleString('en-US', {
+ dateStyle: 'long',
+ timeStyle: 'long'
+ });
+
+ const commentBody = `
+ ### Temporary Branch Update
+
+ The temporary branch has been updated with the latest changes. Below are the details:
+
+ - **Branch Name**: [${branchName}](${branchUrl})
+ - **Commit Hash**: [${commitHash}](${commitUrl})
+ - **Last Updated**: ${lastUpdatedDate}
+ - **Install Command**: \`npm i github:duckduckgo/content-scope-scripts#${commitHash}\`
+
+ Please use the above install command to update to the latest version.
+ `;
+ core.setOutput('comment_body', commentBody);
+ core.setOutput('pr_number', prNumber);
+
+ - name: Create, or Update the Comment
+ uses: peter-evans/create-or-update-comment@v4
+ with:
+ issue-number: ${{ github.event.pull_request.number }}
+ comment-id: ${{ steps.find_comment.outputs.comment-id }}
+ body: ${{ steps.create_body.outputs.comment_body }}
+ edit-mode: replace
+
+ clean_up:
+ if: github.event.action == 'closed'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - name: Delete release branch
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git push origin --delete pr-releases/pr-${PR_NUMBER}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f9e938f886..e26b1b162d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,52 +1,91 @@
name: Release
on:
- workflow_dispatch:
+ workflow_dispatch:
+ inputs:
+ version:
+ required: true
+ description: 'Release version'
jobs:
- release_pr:
- runs-on: ubuntu-latest
- if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')"
-
- steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
- with:
- node-version: 18
- cache: 'npm'
-
- - name: Fetch files and checkout
- run: |
- git fetch --all
- git checkout releases
-
- - name: Build release
- run: |
- npm ci
- npm run build
-
- - name: Check in files
- run: |
- git checkout releases
- git add -f build/ Sources/
-
- - name: Get version
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- npx auto version
- echo "VERSION=$(npx auto version)" >> $GITHUB_ENV
-
- - name: Commit build files
- uses: stefanzweifel/git-auto-commit-action@v4
- with:
- commit_message: "Release build ${{ env.VERSION }} [ci release]"
- commit_options: '--allow-empty'
- skip_checkout: true
- branch: "releases"
-
- - name: Ship
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- npx auto shipit
+ release_pr:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Fetch files and ensure branches exist
+ run: |
+ git fetch origin
+ if [ -f .git/shallow ]; then
+ echo "Shallow repo clone, unshallowing"
+ git fetch --unshallow
+ fi
+ git fetch --tags
+ # Check if the 'main' branch exists, if not, create it
+ if git rev-parse --verify main >/dev/null 2>&1; then
+ git checkout main
+ else
+ git checkout -b main origin/main
+ fi
+ # Check if the 'releases' branch exists, if not, create it
+ if git rev-parse --verify releases >/dev/null 2>&1; then
+ git checkout releases
+ else
+ git checkout -b releases origin/releases
+ fi
+
+ - name: Collect commit ranges
+ run: |
+ bash ./scripts/changelog.sh > ${{ github.workspace }}/CHANGELOG.txt
+
+ - name: Debug changelog file
+ run: |
+ ls -la ${{ github.workspace }}/CHANGELOG.txt
+ cat ${{ github.workspace }}/CHANGELOG.txt
+ echo "Current tag is: $(git rev-list --tags --max-count=1)"
+
+ - name: Ensure clean release branch from main
+ run: |
+ # Remove all tracked and untracked files except .git and .github
+ find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name '.github' ! -name 'CHANGELOG.txt' -exec rm -rf {} +
+
+ # Copy files from main branch
+ git checkout main -- .
+
+ - name: Build release
+ run: |
+ npm ci
+ npm run build
+
+ - name: Check in files
+ run: |
+ git add -f . ':!CHANGELOG.txt' ':!node_modules'
+
+ - name: Commit build files
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: 'Release build ${{ github.event.inputs.version }} [ci release]'
+ commit_options: '--allow-empty'
+ skip_checkout: true
+ branch: 'releases'
+
+ - name: Debug changelog file
+ run: |
+ ls -la ${{ github.workspace }}/CHANGELOG.txt
+ cat ${{ github.workspace }}/CHANGELOG.txt
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ body_path: ${{ github.workspace }}/CHANGELOG.txt
+ draft: false
+ prerelease: false
+ tag_name: ${{ github.event.inputs.version }}
+ target_commitish: 'releases'
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index d643a68bd9..77fc47c26e 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -1,54 +1,54 @@
-name: "CodeQL"
+name: 'CodeQL'
on:
- push:
- branches: [ develop ]
- pull_request:
- # The branches below must be a subset of the branches above
- branches: [ develop ]
- schedule:
- - cron: '40 11 * * 5'
+ push:
+ branches: [develop]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [develop]
+ schedule:
+ - cron: '40 11 * * 5'
jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'javascript' ]
- # Learn more:
- # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v1
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
-
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v1
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['javascript']
+ # Learn more:
+ # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
new file mode 100644
index 0000000000..97a6701d3b
--- /dev/null
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -0,0 +1,32 @@
+name: Dependabot auto-approve and auto-merge
+on: pull_request
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ if: github.event.pull_request.user.login == 'dependabot[bot]'
+ steps:
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b
+ with:
+ github-token: '${{ secrets.GITHUB_TOKEN }}'
+
+ - name: Auto-approve and enable auto-merge for npm patch updates (except ignored packages)
+ if: |
+ steps.metadata.outputs.package-ecosystem == 'npm' &&
+ steps.metadata.outputs.update-type == 'version-update:semver-patch' &&
+ !contains(steps.metadata.outputs.dependency-names, '@atlaskit/pragmatic-drag-and-drop') &&
+ !contains(steps.metadata.outputs.dependency-names, 'preact') &&
+ !contains(steps.metadata.outputs.dependency-names, '@preact/signals') &&
+ !contains(steps.metadata.outputs.dependency-names, 'lottie-web')
+ run: |
+ gh pr review --approve "$PR_URL"
+ gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/snapshots-update.yml b/.github/workflows/snapshots-update.yml
new file mode 100644
index 0000000000..651a9d9d3d
--- /dev/null
+++ b/.github/workflows/snapshots-update.yml
@@ -0,0 +1,84 @@
+name: Update Snapshots on a PR
+description: |
+ Runs previously failed snapshot tests, and commits the changes.
+
+ Your PR will receive a commit with the changes so you can manually verify before merging.
+
+on:
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: 'Pull Request Number (Warning: This action will push a commit to the referenced PR)'
+ required: true
+ type: number
+
+permissions:
+ pull-requests: write
+ contents: write
+
+jobs:
+ update-pr-with-snapshots:
+ name: Update PR With Snapshots
+ runs-on: macos-14
+ steps:
+ - uses: actions/checkout@v5
+ - name: Checkout PR ${{ github.event.inputs.pr_number }}
+ if: github.event_name == 'workflow_dispatch'
+ run: gh pr checkout ${{ github.event.inputs.pr_number }}
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - uses: actions/cache@v4
+ with:
+ path: |
+ ~/.npm
+ ~/.cache/ms-playwright
+ key: ${{ runner.os }}-node-playwright-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-playwright-
+ ${{ runner.os }}-node-
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build all
+ run: npm run build
+
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+
+ - name: Run Screenshot tests
+ id: screenshot_tests
+ run: npm run test-int-snapshots
+
+ - if: ${{ steps.screenshot_tests.conclusion == 'success' }}
+ run: |
+ echo "nothing to update - tests all passed"
+
+ - name: Re-Running Playwright to update snapshots
+ id: screenshot_tests_update
+ if: ${{ failure() && steps.screenshot_tests.conclusion == 'failure' }}
+ run: npm run test-int-snapshots-update
+
+ - name: Commit the updated files to the PR branch
+ if: ${{ failure() && steps.screenshot_tests_update.conclusion == 'success' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ # Configure Git with a bot's username and email for committing changes
+ # This makes it easy to identify in the PR
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ # Stage all updated PNG files for commit
+ git add "*.png"
+
+ # Commit the changes with a descriptive message
+ git commit -m "Updated snapshots via workflow"
+
+ # Push the changes to the current branch in the PR
+ git push origin HEAD
diff --git a/.github/workflows/snapshots.yml b/.github/workflows/snapshots.yml
new file mode 100644
index 0000000000..4eb880fec7
--- /dev/null
+++ b/.github/workflows/snapshots.yml
@@ -0,0 +1,67 @@
+name: Test Snapshots
+description: |
+ Runs snapshot tests and uploads test reports as artifacts
+
+ If this workflow fails, you can trigger `update-snapshots.yml` from the
+ GitHub UI.
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ merge_group:
+
+permissions:
+ contents: read
+
+jobs:
+ snapshots:
+ timeout-minutes: 5
+ runs-on: macos-14
+ steps:
+ - uses: actions/checkout@v5
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - uses: actions/cache@v4
+ with:
+ path: |
+ ~/.npm
+ ~/.cache/ms-playwright
+ key: ${{ runner.os }}-node-playwright-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-playwright-
+ ${{ runner.os }}-node-
+
+ - run: npm ci
+ - run: npm run build
+
+ - run: npm run lint
+ continue-on-error: true
+
+ - run: npm run stylelint
+ continue-on-error: true
+
+ - run: npm run test-unit
+ continue-on-error: true
+
+ - name: 'Clean tree'
+ run: 'npm run test-clean-tree'
+
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+
+ - run: npm run test-int-snapshots
+
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report-pages
+ path: |
+ special-pages/playwright-report/**
+ special-pages/test-results/**
+ injected/playwright-report/**
+ injected/test-results/**
+ retention-days: 5
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 598dd33418..c463094a4d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,46 +1,128 @@
name: Test
-on: [push, pull_request]
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ merge_group:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+ deployments: write
jobs:
- unit:
- runs-on: ubuntu-20.04
- steps:
- - uses: actions/checkout@v2
- - name: Use Node.js 16
- uses: actions/setup-node@v1
- with:
- node-version: 16.x
- - uses: actions/cache@v2
- with:
- path: ~/.npm
- key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-node-
- - run: npm install
- - run: npm run build
- - run: npm run lint
- - run: npm run test-unit
- - name: "Clean tree"
- run: "npm run test-clean-tree"
- integration:
- runs-on: ubuntu-20.04
- timeout-minutes: 10
- steps:
- - uses: actions/checkout@v2
- - name: Use Node.js 16
- uses: actions/setup-node@v1
- with:
- node-version: 16.x
- - uses: actions/cache@v2
- with:
- path: ~/.npm
- key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-node-
- - name: Install dependencies
- run: |
- sudo apt-get install xvfb
- npm install
- npm run build
- - run: npm run test-int-x
+ unit:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ steps:
+ - uses: actions/checkout@v5
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+ - run: npm ci
+ - run: npm run build
+ - run: npm run lint
+ - run: npm run stylelint
+ - run: npm run test-unit
+ - name: 'Clean tree'
+ run: 'npm run test-clean-tree'
+ integration:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v5
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+ - name: Install dependencies
+ run: |
+ npm install
+ npm run build
+ - name: Cache docs output
+ id: docs-output
+ uses: actions/cache@v4
+ with:
+ path: docs
+ key: docs-output-${{ github.run_id }}
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+ - run: npm run test-int-x
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report-pages
+ path: special-pages/playwright-report
+ retention-days: 5
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report-injected
+ path: injected/playwright-report
+ retention-days: 5
+ - name: Build docs
+ run: npm run docs
+
+ deploy-docs:
+ runs-on: ubuntu-latest
+ needs: integration
+ if: ${{ github.ref == 'refs/heads/main' }}
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/checkout@v5
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - name: Cache build outputs
+ id: docs-output
+ uses: actions/cache@v4
+ with:
+ path: docs
+ key: docs-output-${{ github.run_id }}
+ - name: Setup Github Pages
+ uses: actions/configure-pages@v5
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v4
+ with:
+ path: docs
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+
+ # This job ensures all runtime dependencies for the injected/ subproject are correctly listed in 'dependencies' (not 'devDependencies')
+ # by running the build with only production dependencies installed in injected/. It will fail if any required dependency is missing.
+ production-deps:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.nvmrc'
+ - name: Install production dependencies only (injected/)
+ run: npm ci --production
+ - name: Build with production dependencies (injected/)
+ run: cd injected && npm run build
+ - name: Simulate extension esbuild for GPC feature (production deps only)
+ run: npx esbuild injected/src/features/gpc.js --bundle --outfile=/tmp/gpc-bundle.js
diff --git a/.gitignore b/.gitignore
index 98ffdff28f..f529ab8304 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,17 @@
node_modules/
.swiftpm
.env
+.build/
+build/
+/docs
+/screens
+test-results/
+playwright-report/
+Sources/ContentScopeScripts/dist/
+test-results
+!Sources/ContentScopeScripts/dist/pages/.gitignore
+
+# Local Netlify folder
+.netlify
+# VS Code user config
+.vscode
diff --git a/.nvmrc b/.nvmrc
index b6a7d89c68..2bd5a0a98a 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-16
+22
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000..d0de7f9120
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,13 @@
+build/**/*
+docs/**/*
+!injected/docs/**/*
+injected/src/types
+special-pages/pages/**/types
+injected/integration-test/extension/contentScope.js
+**/*.json
+**/*.md
+!injected/docs/**/*.md
+**/*.html
+!injected/integration-test/test-pages/*
+**/*.har
+**/*.css
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000000..05af754a1c
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "singleQuote": true,
+ "printWidth": 140,
+ "tabWidth": 4
+}
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 0000000000..8b092df8e4
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,41 @@
+{
+ "extends": ["stylelint-config-standard"],
+ "plugins": ["stylelint-csstree-validator"],
+ "ignoreFiles": ["build/**/*.css", "Sources/**/*.css", "docs/**/*.css", "special-pages/pages/**/*/dist/*.css"],
+ "rules": {
+ "csstree/validator": {
+ "ignoreProperties": ["text-wrap", "view-transition-name"]
+ },
+ "alpha-value-notation": null,
+ "at-rule-empty-line-before": null,
+ "color-function-notation": null,
+ "color-hex-length": null,
+ "comment-empty-line-before": null,
+ "custom-property-empty-line-before": null,
+ "custom-property-pattern": null,
+ "declaration-block-no-redundant-longhand-properties": null,
+ "declaration-empty-line-before": null,
+ "function-url-quotes": null,
+ "length-zero-no-unit": null,
+ "media-feature-range-notation": null,
+ "no-descending-specificity": null,
+ "property-no-vendor-prefix": null,
+ "rule-empty-line-before": null,
+ "selector-attribute-quotes": null,
+ "selector-class-pattern": null,
+ "selector-pseudo-element-colon-notation": null,
+ "shorthand-property-no-redundant-values": null,
+ "no-duplicate-selectors": null,
+ "comment-whitespace-inside": null,
+ "declaration-block-no-duplicate-properties": null,
+ "value-keyword-case": null,
+ "keyframes-name-pattern": null,
+ "block-no-empty": null,
+ "selector-id-pattern": null,
+ "selector-pseudo-class-no-unknown": null,
+ "declaration-block-no-shorthand-property-overrides": null,
+ "font-family-no-missing-generic-family-keyword": null,
+ "font-family-name-quotes": null,
+ "value-no-vendor-prefix": null
+ }
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 5f0f8a782b..0000000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "files.exclude": {
- "**/build/*": true,
- "Sources/ContentScopeScripts/dist/contentScope.js": true
- }
-}
\ No newline at end of file
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
new file mode 100644
index 0000000000..ef434197d9
--- /dev/null
+++ b/CHANGELOG.txt
@@ -0,0 +1,5 @@
+- Add timeout to enumerateDevices call (#1982)
+- build(deps-dev): bump the eslint group across 1 directory with 2 updates (#1980)
+- build(deps-dev): bump web-ext from 8.9.0 to 8.10.0 (#1978)
+- build(deps): bump esbuild from 0.25.9 to 0.25.10 (#1976)
+- build(deps-dev): bump wait-on from 8.0.5 to 9.0.1 (#1975)
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
index a2af625d5b..a290d974ae 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,14 +1,37 @@
-* @jonathanKingston @shakyShane
+* @duckduckgo/content-scope-scripts-owners
+
+# Documentation - anyone can edit
+injected/docs/
# Feature owners
-src/features/fingerprinting-* @jonathanKingston @englehardt
-src/canvas.js @jonathanKingston @englehardt
-src/element-hiding.js @jonathanKingston @dharb
-src/features/click-to-play.js @kzar @ladamski
-src/locales/click-to-play/ @kzar @ladamski
+injected/src/features/fingerprinting-* @duckduckgo/content-scope-scripts-owners @jonathanKingston @englehardt
+injected/src/canvas.js @duckduckgo/content-scope-scripts-owners @jonathanKingston @englehardt
+injected/src/element-hiding.js @duckduckgo/content-scope-scripts-owners @jonathanKingston @dharb
+injected/src/features/click-to-load.js @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
+injected/src/features/click-to-load/ @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
+injected/src/locales/click-to-load/ @duckduckgo/content-scope-scripts-owners @kzar @ladamski @franfaccin @jonathanKingston @shakyShane
+injected/src/features/autofill-password-import.js @duckduckgo/content-scope-scripts-owners @dbajpeyi
+
+# Broker protection
+injected/src/features/broker-protection.js @duckduckgo/content-scope-scripts-owners @duckduckgo/injected-broker-protection
+injected/src/features/broker-protection/ @duckduckgo/content-scope-scripts-owners @duckduckgo/injected-broker-protection
+injected/integration-test/page-objects/broker-protection.js @duckduckgo/content-scope-scripts-owners @duckduckgo/injected-broker-protection
+injected/integration-test/broker-protection-tests/ @duckduckgo/content-scope-scripts-owners @duckduckgo/injected-broker-protection
+injected/integration-test/mocks/broker-protection/ @duckduckgo/content-scope-scripts-owners @duckduckgo/injected-broker-protection
+injected/integration-test/test-pages/broker-protection/ @duckduckgo/content-scope-scripts-owners @duckduckgo/injected-broker-protection
# Platform owners
-inject/android.js @jonathanKingston @joshliebe
-inject/chrome-mv3.js @kzar @sammacbeth
-inject/chrome.js @jonathanKingston @sammacbeth
-inject/windows.js @jonathanKingston @q71114 @szanto90balazs
\ No newline at end of file
+injected/src/features.js @duckduckgo/content-scope-scripts-owners @duckduckgo/apple-devs @duckduckgo/android-devs @duckduckgo/team-windows-development @duckduckgo/extension-owners
+Sources/ @duckduckgo/content-scope-scripts-owners @duckduckgo/apple-devs
+injected/entry-points/android.js @duckduckgo/content-scope-scripts-owners @duckduckgo/android-devs
+injected/entry-points/extension-mv3.js @duckduckgo/content-scope-scripts-owners @duckduckgo/extension-owners
+injected/entry-points/windows.js @duckduckgo/content-scope-scripts-owners @duckduckgo/team-windows-development
+
+# Test owners
+injected/integration-tests/test-pages/ @duckduckgo/content-scope-scripts-owners @kdzwinel @jonathanKingston
+
+# Special Pages
+special-pages/ @duckduckgo/content-scope-scripts-owners @shakyShane @mgurgel
+
+# Others
+types-generator/ @duckduckgo/content-scope-scripts-owners @shakyShane
diff --git a/CODING_STYLE.md b/CODING_STYLE.md
new file mode 100644
index 0000000000..d97df18438
--- /dev/null
+++ b/CODING_STYLE.md
@@ -0,0 +1,246 @@
+# Coding Style Guide
+
+## Overview
+
+This document outlines the coding style and conventions used in the Content Scope Scripts project.
+
+## Code Formatting
+
+### Prettier
+
+We use [Prettier](https://prettier.io/) for automatic code formatting. This ensures consistent code style across the entire codebase.
+
+- **Configuration**: See [`.prettierrc`](.prettierrc) for current formatting settings
+- **IDE Integration**: Enable Prettier in your IDE/editor for automatic formatting on save
+- **CI/CD**: Code formatting is checked in our continuous integration pipeline
+
+### Running Prettier
+
+```bash
+# Format all files
+npm run lint-fix
+
+# Check formatting (without making changes)
+npm run lint
+```
+
+## TypeScript via JSDoc
+
+We use JSDoc comments to provide TypeScript-like type safety without requiring a TypeScript compilation step.
+
+### JSDoc Resources
+
+- https://devhints.io/jsdoc
+- https://docs.joshuatz.com/cheatsheets/js/jsdoc/
+
+### Basic JSDoc Usage
+
+```javascript
+/**
+ * @param {string} videoId - The video identifier
+ * @param {() => void} handler - Callback function to invoke
+ * @returns {boolean} Whether the operation was successful
+ */
+function processVideo(videoId, handler) {
+ // implementation
+}
+```
+
+### Type Annotations
+
+```javascript
+// Variable type annotation
+/** @type {HTMLElement|null} */
+const element = document.getElementById('my-element');
+
+// Function parameter and return types
+/**
+ * @param {HTMLIFrameElement} iframe
+ * @returns {(() => void)|null}
+ */
+function setupIframe(iframe) {
+ // implementation
+}
+```
+
+### Interface Definitions
+
+```javascript
+/**
+ * @typedef {Object} VideoParams
+ * @property {string} id - Video ID
+ * @property {string} title - Video title
+ * @property {number} duration - Duration in seconds
+ */
+
+/**
+ * @typedef {import("./iframe").IframeFeature} IframeFeature
+ */
+```
+
+## Safety and Defensive Programming
+
+### Type Guards Over Type Casting
+
+When working with DOM elements or external environments (like iframes), prefer runtime type checks over type casting:
+
+```javascript
+// ❌ Avoid type casting when safety is uncertain
+/** @type {Element} */
+const element = someUnknownValue;
+
+// ✅ Use instanceof checks for runtime safety
+if (!(target instanceof Element)) return;
+const element = target; // TypeScript now knows this is an Element
+```
+
+### Null/Undefined Checks
+
+Always check for null/undefined when accessing properties that might not exist:
+
+```javascript
+// ❌ Unsafe
+const doc = iframe.contentDocument;
+doc.addEventListener('click', handler);
+
+// ✅ Safe
+const doc = iframe.contentDocument;
+if (!doc) {
+ console.log('could not access contentDocument');
+ return;
+}
+doc.addEventListener('click', handler);
+```
+
+## Best Practices
+
+1. **Use meaningful variable names** that describe their purpose
+2. **Add JSDoc comments** for all public functions and complex logic
+3. **Prefer explicit type checks** over type assertions in uncertain environments
+4. **Handle edge cases** gracefully with proper error handling
+5. **Keep functions small and focused** on a single responsibility
+6. **Design richer return types** to avoid using exceptions as control flow
+7. **Favor implements over extends** to avoid class inheritance (see Interface Implementation section)
+8. **Remove 'index' files** if they only serve to enable re-exports - prefer explicit imports/exports
+9. **Prefer function declarations** over arrow functions for module-level functions
+
+### Return Types and Error Handling
+
+Instead of using exceptions for control flow, design richer return types:
+
+```javascript
+// ❌ Using exceptions for control flow
+function parseVideoId(url) {
+ if (!url) throw new Error('URL is required');
+ if (!isValidUrl(url)) throw new Error('Invalid URL');
+ return extractId(url);
+}
+
+// ✅ Using richer return types
+/**
+ * @typedef {Object} ParseResult
+ * @property {boolean} success
+ * @property {string} [videoId] - Present when success is true
+ * @property {string} [error] - Present when success is false
+ */
+
+/**
+ * @param {string} url
+ * @returns {ParseResult}
+ */
+function parseVideoId(url) {
+ if (!url) return { success: false, error: 'URL is required' };
+ if (!isValidUrl(url)) return { success: false, error: 'Invalid URL' };
+ return { success: true, videoId: extractId(url) };
+}
+```
+
+### Interface Implementation
+
+Favor `implements` over `extends` to avoid class inheritance. While this is awkward in JSDoc, it promotes composition over inheritance:
+
+```javascript
+/**
+ * @typedef {Object} IframeFeature
+ * @property {function(HTMLIFrameElement): void} iframeDidLoad
+ */
+
+/**
+ * @implements {IframeFeature}
+ */
+export class ReplaceWatchLinks {
+ /**
+ * @param {HTMLIFrameElement} iframe
+ */
+ iframeDidLoad(iframe) {
+ // implementation
+ }
+}
+```
+
+### Import/Export Patterns
+
+Avoid index files that only serve re-exports. Be explicit about imports:
+
+```javascript
+// ❌ Avoid index.js files with only re-exports
+// index.js
+export { FeatureA } from './feature-a.js';
+export { FeatureB } from './feature-b.js';
+
+// ✅ Import directly from source files
+import { FeatureA } from './features/feature-a.js';
+import { FeatureB } from './features/feature-b.js';
+```
+
+### Function Declarations
+
+Prefer function declarations over arrow functions for module-level functions:
+
+```javascript
+// ❌ Arrow functions at module level
+const processVideo = (videoId) => {
+ // implementation
+};
+
+const validateUrl = (url) => {
+ // implementation
+};
+
+// ✅ Function declarations at module level
+function processVideo(videoId) {
+ // implementation
+}
+
+function validateUrl(url) {
+ // implementation
+}
+```
+
+**Why function declarations are preferred:**
+- Hoisted, so order doesn't matter
+- Cleaner syntax for longer functions
+- Better stack traces in debugging
+- More conventional for module-level exports
+
+## IDE Configuration
+
+### VS Code
+
+Recommended extensions:
+- Prettier - Code formatter
+- TypeScript and JavaScript Language Features (built-in)
+- ESLint
+
+
+## Linting
+
+We use ESLint for code quality checks. See [`eslint.config.js`](eslint.config.js) for the current linting configuration.
+
+Run linting with:
+
+```bash
+npm run lint
+```
+
+Follow the linting rules and fix any issues before committing code.
\ No newline at end of file
diff --git a/Package.swift b/Package.swift
deleted file mode 100644
index 8f8d6b23f2..0000000000
--- a/Package.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-// swift-tools-version:5.3
-// The swift-tools-version declares the minimum version of Swift required to build this package.
-
-import PackageDescription
-
-let package = Package(
- name: "ContentScopeScripts",
- products: [
- // Products define the executables and libraries a package produces, and make them visible to other packages.
- .library(
- name: "ContentScopeScripts",
- targets: ["ContentScopeScripts"]),
- ],
- dependencies: [
- ],
- targets: [
- // Targets are the basic building blocks of a package. A target can define a module or a test suite.
- // Targets can depend on other targets in this package, and on products in packages this package depends on.
- .target(
- name: "ContentScopeScripts",
- dependencies: [],
- resources: [
- .process("dist")
- ]
- ),
- ]
-)
diff --git a/README.md b/README.md
index 055a0b2968..a8b2625204 100644
--- a/README.md
+++ b/README.md
@@ -1,80 +1,122 @@
-# Content scope scripts
-
-Content Scope Scripts handles injecting in DOM modifications in a browser context; it's a cross platform solution that requires some minimal platform hooks.
-
-## Content Scope Features API
-
-Each platform calls into the API exposed by content-scope-features.js where the relevant JavaScript file is included from features/. This file loads the relevant platform enabled features. The platform itself should adhere to the features lifecycle when implementing.
-
-The exposed API is a global called contentScopeFeatures and has three methods:
-- load
- - Calls the load method on all of the features
-- init
- - Calls the init method on all of the features
- - This should be passed the arguments object which has the following keys:
- - 'platform' which is an object with:
- - 'name' which is a string of 'android', 'ios', 'macos' or 'extension'
- - 'debug' true if debuging should be enabled
- - 'globalPrivacyControlValue' false if the user has disabled GPC.
- - 'sessionKey' a unique session based key.
- - 'cookie' TODO
- - 'site' which is an object with:
- - 'isBroken' true if remote config has an exception.
- - 'allowlisted' true if the user has disabled protections.
- - 'domain' the hostname of the site in the URL bar
- - 'enabledFeatures' this is an array of features/ to enable
-- update
- - Calls the update method on all of the features
-
-## Features
-
-These files stored in the features directory must include an init function and optionally update and load explained in the features lifecycle.
-
-## Features Lifecycle
-
-There are three stages that the content scope code is hooked into the platform:
-- load
- - This should be reserved for work that should happen that could cause a delay in loading the feature.
- - Given the current limitations of how we inject our code we don't have the Privacy Remote Configuration exceptions so authors should be wary of actually loading anything that would modify the page (and potentially breaking it).
- - This limitation may be re-addressed in manifest v3
- - One exception here is the first party cookie protections that are triggered on init to prevent race conditions.
-- init
- - This is the main place that features are actually loaded into the extension.
-- update
- - This allows the feature to be sent updates from the browser.
- - If this is triggered before init, these updates will be queued and triggered straight after.
-
-### Platform specific integration details
-
-The [inject/](https://github.com/duckduckgo/content-scope-scripts/tree/main/inject) directory handles platform specific differences and is glue code into calling the contentScopeFeatures API.
-
-- In Firefox the code is loaded as a standard extension content script.
-- For Apple, Windows and Android the code is a UserScript that has some string replacements for properties and loads in as the page scope.
- - Note: currently we don't implement the update calls as it's only required by cookie protections which we don't implement.
-- All other browsers the code is stringified, base64 encoded and injected in as a self deleting
+
+
+
+
+
+
+