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/.github/scripts/diff-directories.js b/.github/scripts/diff-directories.js
index 2f1219b170..8d1b5408ec 100644
--- a/.github/scripts/diff-directories.js
+++ b/.github/scripts/diff-directories.js
@@ -26,7 +26,7 @@ function upperCaseFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
-function displayDiffs(dir1Files, dir2Files, isOpen) {
+function displayDiffs(dir1Files, dir2Files) {
const rollupGrouping = {};
/**
* Rolls up multiple files with the same diff into a single entry
@@ -79,13 +79,13 @@ function displayDiffs(dir1Files, dir2Files, isOpen) {
}
}
outString += '\n\n' + rollup.string;
- return renderDetails(title, outString, isOpen);
+ return renderDetails(title, outString);
})
.join('\n');
return outString;
}
-function renderDetails(section, text, isOpen) {
+function renderDetails(section, text) {
if (section === 'dist') {
section = 'apple';
}
@@ -113,12 +113,9 @@ function sortFiles(dirFiles, dirName) {
}
const buildDir = '/build';
-const sourcesOutput = '/Sources/ContentScopeScripts/';
sortFiles(readFilesRecursively(dir1 + buildDir), 'dir1');
sortFiles(readFilesRecursively(dir2 + buildDir), 'dir2');
-sortFiles(readFilesRecursively(dir1 + sourcesOutput), 'dir1');
-sortFiles(readFilesRecursively(dir2 + sourcesOutput), 'dir2');
// console.log(Object.keys(files))
-const fileOut = displayDiffs(sections.dir1, sections.dir2, true);
+const fileOut = displayDiffs(sections.dir1, sections.dir2);
console.log(fileOut);
diff --git a/.github/workflows/asana.yml b/.github/workflows/asana.yml
index d536f61d07..7acddeff06 100644
--- a/.github/workflows/asana.yml
+++ b/.github/workflows/asana.yml
@@ -14,10 +14,12 @@ jobs:
sync:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: sammacbeth/action-asana-sync@v6
+ - 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
index 0f051727c3..da6cc069bd 100644
--- a/.github/workflows/auto-respond-pr.yml
+++ b/.github/workflows/auto-respond-pr.yml
@@ -11,14 +11,14 @@ jobs:
steps:
- name: Checkout base branch
- uses: actions/checkout@v4
+ 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@v4
+ uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -49,7 +49,7 @@ jobs:
node pr/.github/scripts/diff-directories.js base pr > diff.txt
- name: Find Previous Comment
- uses: peter-evans/find-comment@v3
+ uses: peter-evans/find-comment@v4
id: find_comment
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -58,7 +58,7 @@ jobs:
direction: last
- name: Create Comment Body
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
id: create_body
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -76,7 +76,7 @@ jobs:
core.setOutput('pr_number', prNumber);
- name: Create, or Update the Comment
- uses: peter-evans/create-or-update-comment@v4
+ uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find_comment.outputs.comment-id }}
diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index c038cfd720..c5ab4cc835 100644
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -13,12 +13,12 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- - name: Use Node.js 20
+ - name: Use Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version-file: '.nvmrc'
- uses: actions/cache@v4
with:
path: ~/.npm
@@ -41,14 +41,14 @@ jobs:
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 Sources
+ 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
+ uses: peter-evans/find-comment@v4
id: find_comment
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -57,7 +57,7 @@ jobs:
direction: last
- name: Create Comment Body
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
id: create_body
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -68,6 +68,13 @@ jobs:
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
@@ -75,15 +82,16 @@ jobs:
- **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
+ uses: peter-evans/create-or-update-comment@v5
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find_comment.outputs.comment-id }}
@@ -95,7 +103,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Delete release branch
env:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 57309c991f..0ae409b0de 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -12,10 +12,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version-file: '.nvmrc'
cache: 'npm'
- name: Fetch files and ensure branches exist
@@ -49,9 +49,12 @@ jobs:
cat ${{ github.workspace }}/CHANGELOG.txt
echo "Current tag is: $(git rev-list --tags --max-count=1)"
- - name: Checkout code from main into release branch
+ - name: Ensure clean release branch from main
run: |
- # Checkout the code of main onto releases
+ # 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
@@ -61,10 +64,10 @@ jobs:
- name: Check in files
run: |
- git add -f build/ Sources/
+ git add -f . ':!CHANGELOG.txt' ':!node_modules'
- name: Commit build files
- uses: stefanzweifel/git-auto-commit-action@v5
+ uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: 'Release build ${{ github.event.inputs.version }} [ci release]'
commit_options: '--allow-empty'
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 56fa9795de..4a694fc836 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v3
+ uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,4 +51,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v4
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 2311724457..be66c07686 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -5,6 +5,7 @@ on:
branches:
- main
pull_request:
+ merge_group:
permissions:
contents: read
@@ -19,11 +20,11 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- - uses: actions/checkout@v4
- - name: Use Node.js 20
+ - uses: actions/checkout@v5
+ - name: Use Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version-file: '.nvmrc'
- uses: actions/cache@v4
with:
path: ~/.npm
@@ -39,13 +40,13 @@ jobs:
run: 'npm run test-clean-tree'
integration:
runs-on: ubuntu-latest
- timeout-minutes: 10
+ timeout-minutes: 15
steps:
- - uses: actions/checkout@v4
- - name: Use Node.js 20
+ - uses: actions/checkout@v5
+ - name: Use Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version-file: '.nvmrc'
- uses: actions/cache@v4
with:
path: ~/.npm
@@ -64,14 +65,18 @@ jobs:
key: docs-output-${{ github.run_id }}
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- - name: Install dependencies for CI integration tests
- run: sudo apt-get install xvfb
- run: npm run test-int-x
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-pages
- path: special-pages/test-results
+ 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
@@ -84,11 +89,11 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- - uses: actions/checkout@v4
- - name: Use Node.js 20
+ - uses: actions/checkout@v5
+ - name: Use Node.js
uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version-file: '.nvmrc'
- name: Cache build outputs
id: docs-output
uses: actions/cache@v4
@@ -98,9 +103,26 @@ jobs:
- name: Setup Github Pages
uses: actions/configure-pages@v5
- name: Upload artifact
- uses: actions/upload-pages-artifact@v3
+ 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 f529ab8304..a87bc72bd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,9 @@ Sources/ContentScopeScripts/dist/
test-results
!Sources/ContentScopeScripts/dist/pages/.gitignore
+# Test output files (generated during tests)
+injected/unit-test/fixtures/page-context/output/
+
# Local Netlify folder
.netlify
# VS Code user config
diff --git a/.nvmrc b/.nvmrc
index 209e3ef4b6..2bd5a0a98a 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20
+22
diff --git a/.prettierignore b/.prettierignore
index 3ee510be90..d0de7f9120 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,10 +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/.stylelintrc.json b/.stylelintrc.json
index eeb5e24139..a825e898d0 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -4,8 +4,14 @@
"ignoreFiles": ["build/**/*.css", "Sources/**/*.css", "docs/**/*.css", "special-pages/pages/**/*/dist/*.css"],
"rules": {
"csstree/validator": {
- "ignoreProperties": ["text-wrap"]
+ "ignoreProperties": ["text-wrap", "view-transition-name", "composes"]
},
+ "property-no-unknown": [
+ true,
+ {
+ "ignoreProperties": ["composes"]
+ }
+ ],
"alpha-value-notation": null,
"at-rule-empty-line-before": null,
"color-function-notation": null,
diff --git a/.swift-version b/.swift-version
deleted file mode 100644
index 9ad974f610..0000000000
--- a/.swift-version
+++ /dev/null
@@ -1 +0,0 @@
-5.5
diff --git a/CODEOWNERS b/CODEOWNERS
index 23c6814744..447a7393a0 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,5 +1,8 @@
* @duckduckgo/content-scope-scripts-owners
+# Documentation - anyone can edit
+injected/docs/
+
# Feature owners
injected/src/features/fingerprinting-* @duckduckgo/content-scope-scripts-owners @jonathanKingston @englehardt
injected/src/canvas.js @duckduckgo/content-scope-scripts-owners @jonathanKingston @englehardt
@@ -7,14 +10,22 @@ injected/src/element-hiding.js @duckduckgo/content-scope-scripts-owners @jonatha
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/broker-protection.js @duckduckgo/content-scope-scripts-owners @brianhall @shakyShane
-injected/src/features/broker-protection/ @duckduckgo/content-scope-scripts-owners @brianhall @shakyShane
+injected/src/features/autofill-import.js @duckduckgo/content-scope-scripts-owners @dbajpeyi
+injected/src/features/page-context.js @duckduckgo/content-scope-scripts-owners @noisysocks
+
+# 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
+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 @kzar @sammacbeth
-injected/entry-points/chrome.js @duckduckgo/content-scope-scripts-owners @kzar @sammacbeth
+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
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 5c7881c216..0000000000
--- a/Package.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-// swift-tools-version:5.5
-// 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/contentScope.js"),
- .process("dist/contentScopeIsolated.js"),
- .copy("dist/pages"),
- ]
- ),
- ]
-)
diff --git a/Sources/ContentScopeScripts/ContentScopeScripts.swift b/Sources/ContentScopeScripts/ContentScopeScripts.swift
deleted file mode 100644
index c220f4f98d..0000000000
--- a/Sources/ContentScopeScripts/ContentScopeScripts.swift
+++ /dev/null
@@ -1,5 +0,0 @@
-import Foundation
-
-public struct ContentScopeScripts {
- public static var Bundle: Bundle = .module
-}
diff --git a/Sources/ContentScopeScripts/dist/pages/.gitignore b/Sources/ContentScopeScripts/dist/pages/.gitignore
deleted file mode 100644
index 72e8ffc0db..0000000000
--- a/Sources/ContentScopeScripts/dist/pages/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/eslint.config.js b/eslint.config.js
index 6497113bd9..9d794adf0d 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -11,7 +11,7 @@ export default tseslint.config(
'**/build/',
'**/docs/',
'injected/lib',
- 'Sources/ContentScopeScripts/dist/',
+ 'injected/playwright-report/',
'injected/integration-test/extension/contentScope.js',
'injected/integration-test/test-pages/duckplayer/scripts/dist',
'special-pages/pages/**/public',
diff --git a/injected/README.md b/injected/README.md
index 1f04842ed7..f99f0245fb 100644
--- a/injected/README.md
+++ b/injected/README.md
@@ -1,174 +1,103 @@
-# 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 the features
-- init
- - Calls the init method on all 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 debugging 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 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 [injected/entry-points/](https://github.com/duckduckgo/content-scope-scripts/tree/main/injected/entry-points) 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 `
+
+
+