From e67ac38b9fbd99196c5b9d54a9bfc9402aab945b Mon Sep 17 00:00:00 2001 From: Tashif Ahmad Khan Date: Mon, 6 Oct 2025 14:38:10 +0530 Subject: [PATCH 1/7] feat: add script to generate browser-specific manifest.json files and copy assets --- scripts/generate-manifest.js | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts/generate-manifest.js diff --git a/scripts/generate-manifest.js b/scripts/generate-manifest.js new file mode 100644 index 0000000..254614d --- /dev/null +++ b/scripts/generate-manifest.js @@ -0,0 +1,99 @@ +// Script to generate browser-specific manifest.json files and copy assets (icons, src) +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..'); +const baseManifest = require(path.join(root, 'manifest.json')); + +function safeMkdir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function copyRecursive(src, dest) { + try { + // fs.cpSync is available in Node 16.7+. Use it when possible for simplicity. + if (fs.cpSync) { + fs.cpSync(src, dest, { recursive: true }); + return true; + } + } catch (e) { + // fallthrough to manual copy + } + + // Fallback manual copy + if (!fs.existsSync(src)) return false; + safeMkdir(dest); + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + copyRecursive(srcPath, destPath); + } else if (entry.isSymbolicLink()) { + const real = fs.readlinkSync(srcPath); + try { fs.symlinkSync(real, destPath); } catch (e) { fs.copyFileSync(srcPath, destPath); } + } else { + fs.copyFileSync(srcPath, destPath); + } + } + return true; +} + +function listFilesCount(dir) { + if (!fs.existsSync(dir)) return 0; + let count = 0; + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const p = path.join(dir, entry.name); + if (entry.isDirectory()) count += listFilesCount(p); + else count += 1; + } + return count; +} + +function generateManifestFor(browser) { + const manifest = { ...baseManifest }; + if (browser === 'chrome') { + manifest.manifest_version = 3; + // chrome-specific tweaks can go here + } else if (browser === 'firefox') { + // Many Firefox add-ons still accept manifest v2; we intentionally downgrade + // to v2 for compatibility if needed. Adjust as required. + manifest.manifest_version = 2; + manifest.browser_specific_settings = manifest.browser_specific_settings || {}; + manifest.browser_specific_settings.gecko = manifest.browser_specific_settings.gecko || { + id: '', + strict_min_version: '57.0' + }; + } + + const outDir = path.join(root, 'dist', browser); + safeMkdir(outDir); + fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2)); + + // Copy icons and src + const iconsSrc = path.join(root, 'icons'); + const srcSrc = path.join(root, 'src'); + let copiedIcons = false; + let copiedSrc = false; + try { + if (fs.existsSync(iconsSrc)) { + copiedIcons = copyRecursive(iconsSrc, path.join(outDir, 'icons')); + } + if (fs.existsSync(srcSrc)) { + copiedSrc = copyRecursive(srcSrc, path.join(outDir, 'src')); + } + } catch (err) { + console.error('Error copying assets:', err); + } + + console.log(`${browser} bundle generated at ${outDir}`); +} + +const target = process.argv[2]; +if (target === 'chrome') generateManifestFor('chrome'); +else if (target === 'firefox') generateManifestFor('firefox'); +else { + generateManifestFor('chrome'); + generateManifestFor('firefox'); +} From e07842a5b64b6133901011ff9a8d162dacb11443 Mon Sep 17 00:00:00 2001 From: Tashif Ahmad Khan Date: Mon, 6 Oct 2025 14:38:53 +0530 Subject: [PATCH 2/7] feat: add scripts to package.json for generating browser-specific manifest files --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 66a9cb1..ef0c4a7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "version": "1.0.0", "description": "Turn YouTube playlists into courses. Track your playlist with checkmarks, progress bar, total and watched duration, and completion percentage.", "scripts": { - "format": "prettier --write ." + "format": "prettier --write .", + "manifest:chrome": "node scripts/generate-manifest.js chrome", + "manifest:firefox": "node scripts/generate-manifest.js firefox", + "manifest:all": "node scripts/generate-manifest.js" }, "devDependencies": { "prettier": "^3.6.2" From 4edfaa66e8bbc961e9e0008e4f43a2822c11b95d Mon Sep 17 00:00:00 2001 From: Tashif Ahmad Khan Date: Mon, 6 Oct 2025 14:39:21 +0530 Subject: [PATCH 3/7] chore: add .env, dist/, and .DS_Store to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c2658d7..ab5e67e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ node_modules/ +.env +dist/ +.DS_Store \ No newline at end of file From 839fa8c456104c987cb058a56dc85847b54755b4 Mon Sep 17 00:00:00 2001 From: Tashif Ahmad Khan Date: Mon, 6 Oct 2025 14:39:29 +0530 Subject: [PATCH 4/7] docs: add instructions for building browser-specific bundles in README.md --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 7990cc9..fbd0ae5 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,25 @@ Follow these steps to set up the project on your local machine. > **Note:** Changes to the source code will only reflect after you reload the extension on the `chrome://extensions/` page. +### Building browser bundles (manifests) + +This project includes a small script to generate browser-specific bundles (manifests and assets) into `dist/`. + +Run the generator: + +```bash +# generate Chrome bundle +npm run manifest:chrome + +# generate Firefox bundle +npm run manifest:firefox + +# generate both +npm run manifest:all +``` + +The script will copy `icons/` and `src/` into `dist//`. + ### Project Structure ``` From 3311b590826c1606777cc489d9a9402098876643 Mon Sep 17 00:00:00 2001 From: Alok Yadav Date: Sun, 12 Oct 2025 10:44:14 +0530 Subject: [PATCH 5/7] refactor: build script to generate dist --- package-lock.json | 143 +++++++++++++++++++++++++++++++++++ package.json | 8 +- scripts/build.js | 24 ++++++ scripts/generate-manifest.js | 99 ------------------------ 4 files changed, 172 insertions(+), 102 deletions(-) create mode 100644 scripts/build.js delete mode 100644 scripts/generate-manifest.js diff --git a/package-lock.json b/package-lock.json index becc376..d81d29f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,103 @@ "name": "track-my-course", "version": "1.0.0", "devDependencies": { + "cross-env": "^10.1.0", + "fs-extra": "^11.3.2", "prettier": "^3.6.2" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/prettier": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", @@ -26,6 +120,55 @@ "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } } } } diff --git a/package.json b/package.json index ef0c4a7..ec57fb5 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,13 @@ "description": "Turn YouTube playlists into courses. Track your playlist with checkmarks, progress bar, total and watched duration, and completion percentage.", "scripts": { "format": "prettier --write .", - "manifest:chrome": "node scripts/generate-manifest.js chrome", - "manifest:firefox": "node scripts/generate-manifest.js firefox", - "manifest:all": "node scripts/generate-manifest.js" + "build:chrome": "cross-env TARGET_BROWSER=chrome node scripts/build.js", + "build:firefox": "cross-env TARGET_BROWSER=firefox node scripts/build.js", + "build": "node scripts/build.js" }, "devDependencies": { + "cross-env": "^10.1.0", + "fs-extra": "^11.3.2", "prettier": "^3.6.2" } } diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..ed27463 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,24 @@ +const fs = require("fs-extra"); +const path = require("path"); + +// Get target browser from env variable, or build both if not set +const target = process.env.TARGET_BROWSER; // "chrome" or "firefox" +const browsers = target ? [target] : ["chrome", "firefox"]; + +browsers.forEach((browser) => { + const distPath = path.join("dist", browser); + + // 1. Clean destination folder + fs.emptyDirSync(distPath); + + // 2. Copy required directories + fs.copySync("src", path.join(distPath, "src")); + fs.copySync("icons", path.join(distPath, "icons")); + + // 3. Copy the correct manifest + const manifestSrc = path.join("manifests", `manifest.${browser}.json`); + const manifestDest = path.join(distPath, "manifest.json"); + fs.copyFileSync(manifestSrc, manifestDest); + + console.log(`✅ ${browser} build created at ${distPath}`); +}); diff --git a/scripts/generate-manifest.js b/scripts/generate-manifest.js deleted file mode 100644 index 254614d..0000000 --- a/scripts/generate-manifest.js +++ /dev/null @@ -1,99 +0,0 @@ -// Script to generate browser-specific manifest.json files and copy assets (icons, src) -const fs = require('fs'); -const path = require('path'); - -const root = path.join(__dirname, '..'); -const baseManifest = require(path.join(root, 'manifest.json')); - -function safeMkdir(dir) { - fs.mkdirSync(dir, { recursive: true }); -} - -function copyRecursive(src, dest) { - try { - // fs.cpSync is available in Node 16.7+. Use it when possible for simplicity. - if (fs.cpSync) { - fs.cpSync(src, dest, { recursive: true }); - return true; - } - } catch (e) { - // fallthrough to manual copy - } - - // Fallback manual copy - if (!fs.existsSync(src)) return false; - safeMkdir(dest); - const entries = fs.readdirSync(src, { withFileTypes: true }); - for (const entry of entries) { - const srcPath = path.join(src, entry.name); - const destPath = path.join(dest, entry.name); - if (entry.isDirectory()) { - copyRecursive(srcPath, destPath); - } else if (entry.isSymbolicLink()) { - const real = fs.readlinkSync(srcPath); - try { fs.symlinkSync(real, destPath); } catch (e) { fs.copyFileSync(srcPath, destPath); } - } else { - fs.copyFileSync(srcPath, destPath); - } - } - return true; -} - -function listFilesCount(dir) { - if (!fs.existsSync(dir)) return 0; - let count = 0; - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const p = path.join(dir, entry.name); - if (entry.isDirectory()) count += listFilesCount(p); - else count += 1; - } - return count; -} - -function generateManifestFor(browser) { - const manifest = { ...baseManifest }; - if (browser === 'chrome') { - manifest.manifest_version = 3; - // chrome-specific tweaks can go here - } else if (browser === 'firefox') { - // Many Firefox add-ons still accept manifest v2; we intentionally downgrade - // to v2 for compatibility if needed. Adjust as required. - manifest.manifest_version = 2; - manifest.browser_specific_settings = manifest.browser_specific_settings || {}; - manifest.browser_specific_settings.gecko = manifest.browser_specific_settings.gecko || { - id: '', - strict_min_version: '57.0' - }; - } - - const outDir = path.join(root, 'dist', browser); - safeMkdir(outDir); - fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2)); - - // Copy icons and src - const iconsSrc = path.join(root, 'icons'); - const srcSrc = path.join(root, 'src'); - let copiedIcons = false; - let copiedSrc = false; - try { - if (fs.existsSync(iconsSrc)) { - copiedIcons = copyRecursive(iconsSrc, path.join(outDir, 'icons')); - } - if (fs.existsSync(srcSrc)) { - copiedSrc = copyRecursive(srcSrc, path.join(outDir, 'src')); - } - } catch (err) { - console.error('Error copying assets:', err); - } - - console.log(`${browser} bundle generated at ${outDir}`); -} - -const target = process.argv[2]; -if (target === 'chrome') generateManifestFor('chrome'); -else if (target === 'firefox') generateManifestFor('firefox'); -else { - generateManifestFor('chrome'); - generateManifestFor('firefox'); -} From 1e20a870f847e28232d5353af7c973915d49adb6 Mon Sep 17 00:00:00 2001 From: Alok Yadav Date: Sun, 12 Oct 2025 11:11:19 +0530 Subject: [PATCH 6/7] feat: add manifest for Firefox --- .../manifest.chrome.json | 0 manifests/manifest.firefox.json | 51 +++++++++++++++++++ 2 files changed, 51 insertions(+) rename manifest.json => manifests/manifest.chrome.json (100%) create mode 100644 manifests/manifest.firefox.json diff --git a/manifest.json b/manifests/manifest.chrome.json similarity index 100% rename from manifest.json rename to manifests/manifest.chrome.json diff --git a/manifests/manifest.firefox.json b/manifests/manifest.firefox.json new file mode 100644 index 0000000..3798760 --- /dev/null +++ b/manifests/manifest.firefox.json @@ -0,0 +1,51 @@ +{ + "manifest_version": 3, + "name": "TrackMyCourse: YouTube Playlist Tracker", + "version": "1.2.0", + "description": "Turn YouTube playlists into courses. Track with checkmarks, progress bar, total and watched duration, and completion percentage.", + "icons": { + "16": "icons/logo_16.png", + "24": "icons/logo_24.png", + "32": "icons/logo_32.png", + "48": "icons/logo_48.png", + "128": "icons/logo_128.png" + }, + "permissions": ["storage", "webNavigation"], + "host_permissions": ["https://www.youtube.com/*", "https://i.ytimg.com/*"], + "background": { + "scripts": ["src/background/background.js"] + }, + "action": { + "default_popup": "src/popup/popup.html", + "default_icon": { + "16": "icons/logo_16.png", + "24": "icons/logo_24.png", + "32": "icons/logo_32.png", + "48": "icons/logo_48.png", + "128": "icons/logo_128.png" + } + }, + "content_scripts": [ + { + "js": ["src/content/toast.js", "src/content/content_script.js"], + "css": [ + "src/styles/toast.css", + "src/styles/checkbox.css", + "src/styles/playlistPage.css", + "src/styles/playlistWatchPage.css", + "src/styles/focusMode.css" + ], + "matches": ["https://www.youtube.com/*"] + } + ], + "browser_specific_settings": { + "gecko": { + "id": "trackmycourse@alok", + "strict_min_version": "110.0", + "data_collection_permissions": { + "required": ["websiteContent"], + "optional": [] + } + } + } +} From a74228f99bd6f01b79bf5b7f912b1e494b25aef2 Mon Sep 17 00:00:00 2001 From: Alok Yadav Date: Sun, 12 Oct 2025 11:33:29 +0530 Subject: [PATCH 7/7] docs: update developer guide and project structure --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fbd0ae5..7896f82 100644 --- a/README.md +++ b/README.md @@ -91,24 +91,24 @@ Follow these steps to set up the project on your local machine. > **Note:** Changes to the source code will only reflect after you reload the extension on the `chrome://extensions/` page. -### Building browser bundles (manifests) +### Building browser bundles -This project includes a small script to generate browser-specific bundles (manifests and assets) into `dist/`. +There is a script `(scripts/build.js)` that generates the Chrome and Firefox bundles into the `dist/` folder. -Run the generator: +Run the build commands: ```bash -# generate Chrome bundle -npm run manifest:chrome +# build Chrome bundle +npm run build:chrome -# generate Firefox bundle -npm run manifest:firefox +# build Firefox bundle +npm run build:firefox -# generate both -npm run manifest:all +# build both Chrome and Firefox +npm run build ``` -The script will copy `icons/` and `src/` into `dist//`. +The script will copy `icons/` and `src/` into `dist//` and include the appropriate manifest (`manifest.chrome.json` or `manifest.firefox.json`) for each browser. ### Project Structure @@ -120,10 +120,12 @@ track-my-course/ │ ├── content/ # Injects scripts directly into web pages. │ ├── popup/ # Code for the extension's popup window. │ └── styles/ # Contains CSS files for UI elements injected onto pages. +├── manifests/ # Browser-specific manifests +│ ├── manifest.chrome.json +│ └── manifest.firefox.json ├── .prettierrc # Prettier configuration for consistent formatting ├── package.json # Project dependencies and scripts -├── package-lock.json # Locked dependency versions -└── manifest.json # Chrome extension configuration file. +└── package-lock.json # Locked dependency versions ``` ---