refactor: Replace Webpack with Vite (#53)

This commit is contained in:
Razboy20
2024-01-24 19:40:30 -06:00
committed by GitHub
parent 1629c85818
commit 0560a01a55
112 changed files with 7322 additions and 32180 deletions

View File

@@ -6,6 +6,10 @@
"node": true, "node": true,
"webextensions": true "webextensions": true
}, },
"ignorePatterns": [
"*.html",
"tsconfig.json"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:react/recommended", "plugin:react/recommended",
@@ -140,7 +144,7 @@
"jsdoc/require-jsdoc": [ "jsdoc/require-jsdoc": [
"warn", "warn",
{ {
"enableFixer": true, "enableFixer": false,
"publicOnly": true, "publicOnly": true,
"checkConstructors": false, "checkConstructors": false,
"require": { "require": {

View File

@@ -1,10 +1,10 @@
# UT Registration Plus # UT Registration Plus
## Built Using: ## Built Using
- React 18 - React 18
- TypeScript - TypeScript
- Webpack 5 (esbuild-loader) - Vite 5
- ESLint - ESLint
- Prettier - Prettier
- Semantic-Release - Semantic-Release
@@ -13,7 +13,6 @@
## Getting Started ## Getting Started
1. Clone this repo 1. Clone this repo
2. Run `npm install` 2. Run `pnpm install` to install and patch all the required dependencies
3. Run `npm start` to start the development server 3. Run `pnpm run dev` to start the development server
4. Run `npm run build` to build the extension for production 4. Run `pnpm build` to build the extension for production
5. Run `npm run release` to release a new version of the extension in CI (either preview or production)

30935
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +1,76 @@
{ {
"name": "ut-registration-plus", "name": "ut-registration-plus",
"version": "0.0.0", "displayName": "UT Registration Plus",
"version": "0.0.1",
"description": "The UT Registration Plus extension is a Chrome extension that allows students to easily register for classes at The University of Texas at Austin.", "description": "The UT Registration Plus extension is a Chrome extension that allows students to easily register for classes at The University of Texas at Austin.",
"private": true, "private": true,
"homepage": "sriramhariharan.com", "homepage": "sriramhariharan.com",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "NODE_ENV=development tsx webpack/development.ts", "dev": "vite",
"build": "NODE_ENV=production tsx webpack/production.ts", "build": "tsc && vite build",
"release": "tsx webpack/release.ts", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"devtools": "react-devtools", "devtools": "react-devtools",
"lint": "eslint ./ --ext .ts,.tsx" "preinstall": "npx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"@types/sql.js": "^1.4.4", "@types/sql.js": "^1.4.9",
"@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.51", "chrome-extension-toolkit": "^0.0.51",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"clean-webpack-plugin": "^4.0.0", "highcharts": "^11.2.0",
"highcharts": "^10.3.3", "highcharts-react-official": "^3.2.1",
"highcharts-react-official": "^3.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"sass": "^1.57.1", "sass": "^1.69.5",
"sql.js": "1.8.0", "sql.js": "1.9.0",
"uuid": "^9.0.0" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@semantic-release/exec": "^6.0.3", "@crxjs/vite-plugin": "2.0.0-beta.21",
"@svgr/webpack": "^6.5.1", "@types/chrome": "^0.0.254",
"@types/chrome": "^0.0.204", "@types/node": "^20.10.5",
"@types/node": "^18.11.17", "@types/prompts": "^2.4.9",
"@types/prompts": "^2.4.2", "@types/react": "^18.2.45",
"@types/react": "^18.0.26", "@types/react-dom": "^18.2.18",
"@types/react-dom": "^18.0.9", "@types/semver": "^7.5.6",
"@types/semver": "^7.3.13", "@types/uuid": "^9.0.7",
"@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/eslint-plugin": "^5.47.0", "@typescript-eslint/parser": "^6.15.0",
"@typescript-eslint/parser": "^5.47.0", "@vitejs/plugin-react-swc": "^3.5.0",
"archiver": "^5.3.1", "cssnano": "^6.0.2",
"case-sensitive-paths-webpack-plugin": "^2.4.0", "cssnano-preset-advanced": "^6.0.2",
"chalk": "^5.2.0",
"conventional-changelog-conventionalcommits": "^5.0.0",
"copy-webpack-plugin": "^11.0.0",
"create-file-webpack": "^1.0.2",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.3",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"esbuild-loader": "^2.20.0", "es-module-lexer": "^1.4.1",
"eslint": "^8.30.0", "eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.5.2", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^39.6.4", "eslint-plugin-jsdoc": "^46.9.1",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.1.1",
"eslint-plugin-react": "^7.31.11", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-prefer-function-component": "^3.1.0", "eslint-plugin-react-prefer-function-component": "^3.3.0",
"file-loader": "^6.2.0", "eslint-plugin-react-refresh": "^0.4.5",
"fork-ts-checker-webpack-plugin": "^7.2.14",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.7.2",
"path": "^0.12.7", "path": "^0.12.7",
"prettier": "^2.8.1", "postcss": "^8.4.32",
"prompts": "^2.4.2", "prettier": "^3.1.1",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"react-devtools": "^4.27.1", "react-devtools": "^4.27.1",
"sass-loader": "^13.2.0", "typescript": "^5.3.3",
"semantic-release": "^19.0.5", "vite": "^5.0.10",
"semver": "^7.3.8", "vite-plugin-inspect": "^0.8.1"
"simple-git": "^3.15.1", },
"socket.io": "^4.5.4", "pnpm": {
"socket.io-client": "^4.5.4", "patchedDependencies": {
"stream-browserify": "^3.0.0", "@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch"
"terser-webpack-plugin": "^5.3.6", },
"ts-node": "^10.9.1", "overrides": {
"tsconfig-paths-webpack-plugin": "^4.0.0", "es-module-lexer": "^1.4.1"
"tsx": "^3.12.1", }
"typescript": "^4.9.5",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-build-notifier": "^2.3.0",
"webpack-dev-server": "^4.11.1"
} }
} }

View File

@@ -0,0 +1,105 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 5c3f6291168987c56b816428080e6f1fe9de7107..abaf6290fe9454ae036a81eacbe7dc3be2fdfbc3 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -499,16 +499,43 @@ ${sourceMap}
}),
mergeMap(async ({ target, code, deps }) => {
await lexer.init;
- const [imports] = lexer.parse(code, fileName);
+ const [imports, exports] = lexer.parse(code, fileName);
const depSet = new Set(deps);
const magic = new MagicString(code);
- for (const i of imports)
+ for (const i of imports) {
if (i.n) {
depSet.add(i.n);
const fileName2 = getFileName({ type: "module", id: i.n });
const fullImport = code.substring(i.s, i.e);
- magic.overwrite(i.s, i.e, fullImport.replace(i.n, `/${fileName2}`));
+ const hmrTimestamp = fullImport.match(/\bt=\d{13}&?\b/);
+ magic.overwrite(
+ i.s,
+ i.e,
+ fullImport.replace(
+ i.n,
+ `/${fileName2}${hmrTimestamp ? `?${hmrTimestamp[0]}` : ""}`
+ )
+ );
+ }
+ }
+ for (const e of exports) {
+ if (e.n === "default") {
+ const regex = /\s+['"](.*)['"]/y;
+ regex.lastIndex = e.e;
+ const fullExport = regex.exec(code)?.[1];
+ if (!fullExport)
+ continue;
+ const start = regex.lastIndex - fullExport.length - 1;
+ const end = regex.lastIndex - 1;
+ if (fullExport.startsWith("/node_modules")) {
+ magic.overwrite(
+ start,
+ end,
+ `http://localhost:5173${fullExport}`
+ );
+ }
}
+ }
return { target, source: magic.toString(), deps: [...depSet] };
})
);
@@ -1229,10 +1256,14 @@ const pluginHMR = () => {
handleHotUpdate({ modules, server }) {
const { root } = server.config;
const relFiles = /* @__PURE__ */ new Set();
- for (const m of modules)
+ function getRelFile(file) {
+ return file.startsWith(root) ? file.slice(server.config.root.length) : file;
+ }
+ for (const m of modules) {
if (m.id?.startsWith(root)) {
relFiles.add(m.id.slice(server.config.root.length));
}
+ }
if (inputManifestFiles.background.length) {
const background = prefix$1("/", inputManifestFiles.background[0]);
if (relFiles.has(background) || modules.some(isImporter(join(server.config.root, background)))) {
@@ -1244,7 +1275,14 @@ const pluginHMR = () => {
for (const [key, script] of contentScripts)
if (key === script.id) {
if (relFiles.has(script.id) || modules.some(isImporter(join(server.config.root, script.id)))) {
- relFiles.forEach((relFile) => update(relFile));
+ modules.filter((mod) => mod.id?.startsWith(root)).forEach((mod) => {
+ update(getRelFile(mod.id));
+ if (mod.file?.endsWith(".scss")) {
+ mod.importers.forEach((imp) => {
+ update(getRelFile(imp.id));
+ });
+ }
+ });
}
}
}
@@ -1882,7 +1920,7 @@ const pluginWebAccessibleResources = () => {
if (contentScripts.size > 0) {
const viteManifest = parseJsonAsset(
bundle,
- "manifest.json"
+ ".vite/manifest.json"
);
const viteFiles = /* @__PURE__ */ new Map();
for (const [, file] of Object.entries(viteManifest))
diff --git a/package.json b/package.json
index e0c47ae66ff399ad3a78abf38d8d93d1f038c55d..f84eb09ffbb5c41094935dd06e04ffe831e2d05a 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"connect-injector": "^0.4.4",
"convert-source-map": "^1.7.0",
"debug": "^4.3.3",
- "es-module-lexer": "^0.10.0",
+ "es-module-lexer": "^1.4.1",
"fast-glob": "^3.2.11",
"fs-extra": "^10.0.1",
"jsesc": "^3.0.2",

6381
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

14
postcss.config.cjs Normal file
View File

@@ -0,0 +1,14 @@
/* eslint-disable global-require */
/** @type {import('postcss-load-config').Config} */
const config = {
plugins:
process.env.NODE_ENV !== 'development'
? [
require('cssnano')({
preset: 'advanced',
}),
]
: [],
};
module.exports = config;

Binary file not shown.

View File

@@ -1,33 +0,0 @@
import io from 'socket.io-client';
import { background } from 'src/shared/messages';
const socket = io('http://localhost:9090');
let reBuilding = false;
socket.on('disconnect', async reason => {
reBuilding = reason.includes('transport') && !reason.includes('client');
});
socket.onAny(args => {
console.log(args);
});
socket.on('connect', async () => {
if (!reBuilding) {
console.log('%c[hot-reloading] listening for changes...', 'color:white; background-color: orange;');
} else {
console.log(
'%c[hot-reloading] changes detected, rebuilding and refreshing...',
'color:white; background-color: orange;'
);
}
});
socket.on('reload', async () => {
console.log('%c[hot-reloading] reloading...', 'color:white; background-color: orange;');
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
if (tabs?.[0]?.id) {
background.reloadExtension();
}
});
});

View File

@@ -1,7 +1,6 @@
import './hotReload'; import { DevStore } from '@shared/storage/DevStore';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { DevStore } from 'src/shared/storage/DevStore'; import { createRoot } from 'react-dom/client';
import render from 'src/views/lib/react';
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
@@ -146,4 +145,4 @@ function DevDashboard() {
); );
} }
render(<DevDashboard />, document.getElementById('root')); createRoot(document.getElementById('root')).render(<DevDashboard />);

14
src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.json' {
const content: string;
export default content;
}

54
src/manifest.ts Normal file
View File

@@ -0,0 +1,54 @@
import { defineManifest } from '@crxjs/vite-plugin';
import packageJson from '../package.json';
// Convert from Semver (example: 0.1.0-beta6)
const [major, minor, patch, label = '0'] = packageJson.version
// can only contain digits, dots, or dash
.replace(/[^\d.-]+/g, '')
// split into version parts
.split(/[.-]/);
const mode = process.env.NODE_ENV;
const HOST_PERMISSIONS: string[] = [
'*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*',
'*://*.utexas.collegescheduler.com/*',
'*://*.catalog.utexas.edu/ribbit/',
'*://*.registrar.utexas.edu/schedules/*',
'*://*.login.utexas.edu/login/*',
];
const manifest = defineManifest(async () => ({
manifest_version: 3,
name: `${packageJson.displayName ?? packageJson.name}${mode === 'development' ? ' (dev)' : ''}`,
version: `${major}.${minor}.${patch}.${label}`,
description: packageJson.description,
options_page: 'src/pages/options/index.html',
background: { service_worker: 'src/pages/background/background.ts' },
permissions: ['storage', 'unlimitedStorage', 'background'],
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
action: {
default_popup: 'src/pages/popup/index.html',
default_icon: `icons/icon_${mode}_32.png`,
},
icons: {
'16': `icons/icon_${mode}_16.png`,
'32': `icons/icon_${mode}_32.png`,
'48': `icons/icon_${mode}_48.png`,
'128': `icons/icon_${mode}_128.png`,
},
content_scripts: [
{
matches: HOST_PERMISSIONS,
js: ['src/pages/content/index.tsx'],
},
],
web_accessible_resources: [
{
resources: ['assets/js/*.js', 'assets/css/*.css', 'assets/img/*'],
matches: ['*://*/*'],
},
],
}));
export default manifest;

View File

@@ -1,10 +1,9 @@
import { BACKGROUND_MESSAGES } from '@shared/messages';
import { MessageListener } from 'chrome-extension-toolkit'; import { MessageListener } from 'chrome-extension-toolkit';
import { BACKGROUND_MESSAGES } from 'src/shared/messages';
import onInstall from './events/onInstall'; import onInstall from './events/onInstall';
import onServiceWorkerAlive from './events/onServiceWorkerAlive'; import onServiceWorkerAlive from './events/onServiceWorkerAlive';
import onUpdate from './events/onUpdate'; import onUpdate from './events/onUpdate';
import browserActionHandler from './handler/browserActionHandler'; import browserActionHandler from './handler/browserActionHandler';
import hotReloadingHandler from './handler/hotReloadingHandler';
import tabManagementHandler from './handler/tabManagementHandler'; import tabManagementHandler from './handler/tabManagementHandler';
import userScheduleHandler from './handler/userScheduleHandler'; import userScheduleHandler from './handler/userScheduleHandler';
@@ -30,7 +29,6 @@ chrome.runtime.onInstalled.addListener(details => {
// initialize the message listener that will listen for messages from the content script // initialize the message listener that will listen for messages from the content script
const messageListener = new MessageListener<BACKGROUND_MESSAGES>({ const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
...browserActionHandler, ...browserActionHandler,
...hotReloadingHandler,
...tabManagementHandler, ...tabManagementHandler,
...userScheduleHandler, ...userScheduleHandler,
}); });

View File

@@ -1,4 +1,4 @@
import { ExtensionStore } from '../../shared/storage/ExtensionStore'; import { ExtensionStore } from '@shared/storage/ExtensionStore';
/** /**
* Called when the extension is first installed or synced onto a new machine * Called when the extension is first installed or synced onto a new machine

View File

@@ -1,5 +1,4 @@
import { hotReloadTab } from 'src/background/util/hotReloadTab'; import { ExtensionStore } from '@shared/storage/ExtensionStore';
import { ExtensionStore } from '../../shared/storage/ExtensionStore';
/** /**
* Called when the extension is updated (or when the extension is reloaded in development mode) * Called when the extension is updated (or when the extension is reloaded in development mode)
@@ -9,8 +8,4 @@ export default async function onUpdate() {
version: chrome.runtime.getManifest().version, version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(), lastUpdate: Date.now(),
}); });
if (process.env.NODE_ENV === 'development') {
hotReloadTab();
}
} }

View File

@@ -1,5 +1,5 @@
import BrowserActionMessages from '@shared/messages/BrowserActionMessages';
import { MessageHandler } from 'chrome-extension-toolkit'; import { MessageHandler } from 'chrome-extension-toolkit';
import BrowserActionMessages from 'src/shared/messages/BrowserActionMessages';
const browserActionHandler: MessageHandler<BrowserActionMessages> = { const browserActionHandler: MessageHandler<BrowserActionMessages> = {
disableBrowserAction({ sender, sendResponse }) { disableBrowserAction({ sender, sendResponse }) {

View File

@@ -1,6 +1,6 @@
import HotReloadingMessages from 'src/shared/messages/HotReloadingMessages'; import HotReloadingMessages from '@shared/messages/HotReloadingMessages';
import { DevStore } from '@shared/storage/DevStore';
import { MessageHandler } from 'chrome-extension-toolkit'; import { MessageHandler } from 'chrome-extension-toolkit';
import { DevStore } from 'src/shared/storage/DevStore';
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = { const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
async reloadExtension({ sendResponse }) { async reloadExtension({ sendResponse }) {

View File

@@ -1,5 +1,5 @@
import TabManagementMessages from '@shared/messages/TabManagementMessages';
import { MessageHandler } from 'chrome-extension-toolkit'; import { MessageHandler } from 'chrome-extension-toolkit';
import TabManagementMessages from 'src/shared/messages/TabManagementMessages';
import openNewTab from '../util/openNewTab'; import openNewTab from '../util/openNewTab';
const tabManagementHandler: MessageHandler<TabManagementMessages> = { const tabManagementHandler: MessageHandler<TabManagementMessages> = {

View File

@@ -1,6 +1,6 @@
import { UserScheduleMessages } from '@shared/messages/UserScheduleMessages';
import { Course } from '@shared/types/Course';
import { MessageHandler } from 'chrome-extension-toolkit'; import { MessageHandler } from 'chrome-extension-toolkit';
import { UserScheduleMessages } from 'src/shared/messages/UserScheduleMessages';
import { Course } from 'src/shared/types/Course';
import addCourse from '../lib/addCourse'; import addCourse from '../lib/addCourse';
import clearCourses from '../lib/clearCourses'; import clearCourses from '../lib/clearCourses';
import createSchedule from '../lib/createSchedule'; import createSchedule from '../lib/createSchedule';

View File

@@ -1,5 +1,5 @@
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { Course } from 'src/shared/types/Course'; import { Course } from '@shared/types/Course';
/** /**
* *

View File

@@ -1,4 +1,4 @@
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
export default async function clearCourses(scheduleName: string): Promise<void> { export default async function clearCourses(scheduleName: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules'); const schedules = await UserScheduleStore.get('schedules');

View File

@@ -1,4 +1,4 @@
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/** /**
* Creates a new schedule with the given name * Creates a new schedule with the given name

View File

@@ -1,4 +1,4 @@
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
export default async function deleteSchedule(scheduleName: string): Promise<string | undefined> { export default async function deleteSchedule(scheduleName: string): Promise<string | undefined> {
const [schedules, activeIndex] = await Promise.all([ const [schedules, activeIndex] = await Promise.all([

View File

@@ -1,5 +1,5 @@
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { Course } from 'src/shared/types/Course'; import { Course } from '@shared/types/Course';
/** /**
* *

View File

@@ -1,4 +1,4 @@
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
export default async function renameSchedule(scheduleName: string, newName: string): Promise<string | undefined> { export default async function renameSchedule(scheduleName: string, newName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules'); const schedules = await UserScheduleStore.get('schedules');

View File

@@ -1,4 +1,4 @@
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
export default async function switchSchedule(scheduleName: string): Promise<void> { export default async function switchSchedule(scheduleName: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules'); const schedules = await UserScheduleStore.get('schedules');

View File

@@ -1,4 +1,4 @@
import { DevStore } from 'src/shared/storage/DevStore'; import { DevStore } from '@shared/storage/DevStore';
/** /**
* A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao) * A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao)
@@ -40,5 +40,3 @@ export async function hotReloadTab(): Promise<void> {
} }
}); });
} }

View File

@@ -1,4 +1,4 @@
import { DevStore } from 'src/shared/storage/DevStore'; import { DevStore } from '@shared/storage/DevStore';
/** /**
* Open the debug tab as the first tab * Open the debug tab as the first tab

View File

@@ -0,0 +1,10 @@
import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
export default function CalendarMain() {
return (
<ExtensionRoot>
<div>Calendar Placeholder</div>
</ExtensionRoot>
);
}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Calendar</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="./index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import CalendarMain from './CalendarMain';
createRoot(document.getElementById('root')).render(<CalendarMain />);

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import CourseCatalogMain from '@views/components/CourseCatalogMain';
import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport';
const support = getSiteSupport(window.location.href);
if (support === SiteSupport.COURSE_CATALOG_DETAILS || support === SiteSupport.COURSE_CATALOG_LIST) {
const container = document.createElement('div');
container.id = 'extension-root';
document.body.appendChild(container);
createRoot(container).render(
<React.StrictMode>
<CourseCatalogMain support={support} />
</React.StrictMode>
);
}

13
src/pages/debug/App.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
/**
*
*/
export default function App() {
return (
<ExtensionRoot>
<div>hello how are you doing today.</div>
</ExtensionRoot>
);
}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Debug</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="./index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1 @@
import 'src/debug';

13
src/pages/options/App.tsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
/**
*
*/
export default function App() {
return (
<ExtensionRoot>
<div>hello how are you doing today.</div>
</ExtensionRoot>
);
}

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Popup</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="./index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(<App />);

View File

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Popup</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="./index.tsx" type="module"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import PopupMain from '../../views/components/PopupMain';
createRoot(document.getElementById('root')).render(<PopupMain />);

View File

@@ -1,17 +1,13 @@
import { createMessenger } from 'chrome-extension-toolkit'; import { createMessenger } from 'chrome-extension-toolkit';
import TAB_MESSAGES from './TabMessages';
import BrowserActionMessages from './BrowserActionMessages'; import BrowserActionMessages from './BrowserActionMessages';
import HotReloadingMessages from './HotReloadingMessages';
import TabManagementMessages from './TabManagementMessages'; import TabManagementMessages from './TabManagementMessages';
import TAB_MESSAGES from './TabMessages';
import { UserScheduleMessages } from './UserScheduleMessages'; import { UserScheduleMessages } from './UserScheduleMessages';
/** /**
* This is a type with all the message definitions that can be sent TO the background script * This is a type with all the message definitions that can be sent TO the background script
*/ */
export type BACKGROUND_MESSAGES = BrowserActionMessages & export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & UserScheduleMessages;
TabManagementMessages &
HotReloadingMessages &
UserScheduleMessages;
/** /**
* A utility object that can be used to send type-safe messages to the background script * A utility object that can be used to send type-safe messages to the background script

View File

@@ -24,10 +24,4 @@ export const DevStore = createLocalStore<IDevStore>({
reloadTabId: undefined, reloadTabId: undefined,
}); });
debugStore({ devStore: DevStore }); debugStore({ devStore: DevStore });

View File

@@ -9,7 +9,7 @@ interface IOptionsStore {
/** whether we should automatically scroll to load more courses on the course schedule page (without having to click next) */ /** whether we should automatically scroll to load more courses on the course schedule page (without having to click next) */
shouldScrollToLoad: boolean; shouldScrollToLoad: boolean;
url: URL; // url: URL;
} }
export const OptionsStore = createSyncStore<IOptionsStore>({ export const OptionsStore = createSyncStore<IOptionsStore>({

View File

@@ -1,5 +1,5 @@
import { UserSchedule } from '@shared/types/UserSchedule';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
import { UserSchedule } from 'src/shared/types/UserSchedule';
interface IUserScheduleStore { interface IUserScheduleStore {
schedules: UserSchedule[]; schedules: UserSchedule[];

View File

@@ -11,7 +11,4 @@
"node" "node"
], ],
}, },
"exclude": [
"../webpack"
]
} }

View File

@@ -1,5 +1,5 @@
import { Course, ScrapedRow } from '@shared/types/Course';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Course, ScrapedRow } from 'src/shared/types/Course';
import { useKeyPress } from '../hooks/useKeyPress'; import { useKeyPress } from '../hooks/useKeyPress';
import useSchedules from '../hooks/useSchedules'; import useSchedules from '../hooks/useSchedules';
import { CourseCatalogScraper } from '../lib/CourseCatalogScraper'; import { CourseCatalogScraper } from '../lib/CourseCatalogScraper';
@@ -7,13 +7,12 @@ import getCourseTableRows from '../lib/getCourseTableRows';
import { SiteSupport } from '../lib/getSiteSupport'; import { SiteSupport } from '../lib/getSiteSupport';
import { populateSearchInputs } from '../lib/populateSearchInputs'; import { populateSearchInputs } from '../lib/populateSearchInputs';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
import Icon from './common/Icon/Icon';
import Text from './common/Text/Text';
import AutoLoad from './injected/AutoLoad/AutoLoad'; import AutoLoad from './injected/AutoLoad/AutoLoad';
import CoursePopup from './injected/CoursePopup/CoursePopup'; import CoursePopup from './injected/CoursePopup/CoursePopup';
import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner'; import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
import TableHead from './injected/TableHead'; import TableHead from './injected/TableHead';
import TableRow from './injected/TableRow/TableRow'; import TableRow from './injected/TableRow/TableRow';
import TableSubheading from './injected/TableSubheading/TableSubheading';
interface Props { interface Props {
support: SiteSupport.COURSE_CATALOG_DETAILS | SiteSupport.COURSE_CATALOG_LIST; support: SiteSupport.COURSE_CATALOG_DETAILS | SiteSupport.COURSE_CATALOG_LIST;
@@ -33,7 +32,7 @@ export default function CourseCatalogMain({ support }: Props) {
useEffect(() => { useEffect(() => {
const tableRows = getCourseTableRows(document); const tableRows = getCourseTableRows(document);
const ccs = new CourseCatalogScraper(support); const ccs = new CourseCatalogScraper(support);
const scrapedRows = ccs.scrape(tableRows); const scrapedRows = ccs.scrape(tableRows, true);
setRows(scrapedRows); setRows(scrapedRows);
}, [support]); }, [support]);
@@ -64,10 +63,10 @@ export default function CourseCatalogMain({ support }: Props) {
<ExtensionRoot> <ExtensionRoot>
<RecruitmentBanner /> <RecruitmentBanner />
<TableHead>Plus</TableHead> <TableHead>Plus</TableHead>
{rows.map(row => { {rows.map((row, i) => {
if (!row.course) { if (!row.course) {
// TODO: handle the course section headers // TODO: handle the course section headers
return null; return <TableSubheading key={row.element.innerText + i.toString()} row={row} />;
} }
return ( return (
<TableRow <TableRow

View File

@@ -1,6 +0,0 @@
import React from 'react';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
export default function MyCalendarMain() {
return <ExtensionRoot>MyCalendarMain</ExtensionRoot>;
}

View File

@@ -1,5 +1,5 @@
import { background } from '@shared/messages';
import React from 'react'; import React from 'react';
import { background } from 'src/shared/messages';
import useSchedules from '../hooks/useSchedules'; import useSchedules from '../hooks/useSchedules';
import { Button } from './common/Button/Button'; import { Button } from './common/Button/Button';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';

View File

@@ -1,4 +1,5 @@
@import 'src/views/styles/base.module.scss'; @use 'sass:color';
@use 'src/views/styles/colors.module.scss';
.button { .button {
background-color: #000; background-color: #000;
@@ -7,9 +8,8 @@
margin: 10px; margin: 10px;
border-radius: 8px; border-radius: 8px;
border: none; border: none;
box-shadow: rgba(0, 0, 0, 0.4) 2px 2px 4px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease-in-out; transition: all 0.1s ease-in-out;
font-family: 'Inter'; font-family: 'Inter';
display: flex; display: flex;
@@ -22,7 +22,7 @@
} }
&:active { &:active {
animation: click_animation 0.2s ease-in-out; transform: scale(0.96);
} }
&.disabled { &.disabled {
@@ -30,20 +30,20 @@
opacity: 0.5 !important; opacity: 0.5 !important;
&:active { &:active {
animation: none !important; transform: unset;
} }
} }
@each $color, @each $color,
$value $value
in ( in (
primary: $burnt_orange, primary: colors.$burnt_orange,
secondary: $charcoal, secondary: colors.$charcoal,
tertiary: $bluebonnet, tertiary: colors.$bluebonnet,
danger: $speedway_brick, danger: colors.$speedway_brick,
warning: $tangerine, warning: colors.$tangerine,
success: $turtle_pond, success: colors.$turtle_pond,
info: $turquoise info: colors.$turquoise
) )
{ {
&.#{$color} { &.#{$color} {
@@ -51,12 +51,12 @@
color: #fff; color: #fff;
&:hover { &:hover {
background-color: lighten($value, 10%); background-color: color.adjust($value, $lightness: 10%);
} }
&:focus, &:focus-visible,
&:active { &:active {
background-color: darken($value, 10%); background-color: color.adjust($value, $lightness: -10%);
} }
&.disabled { &.disabled {
@@ -65,15 +65,3 @@
} }
} }
} }
@keyframes click_animation {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}

View File

@@ -1,7 +1,7 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
.card { .card {
background: $white; background: colors.$white;
border: 0.5px dotted #c3cee0; border: 0.5px dotted #c3cee0;
box-sizing: border-box; box-sizing: border-box;

View File

@@ -1,5 +1,5 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
hr { hr {
border: 1px solid $limestone; border: 1px solid colors.$limestone;
} }

View File

@@ -1,6 +1,6 @@
import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { Color } from 'src/views/styles/colors.module.scss'; import React from 'react';
import { Color } from '@views/styles/colors.module.scss';
import styles from './Divider.module.scss'; import styles from './Divider.module.scss';
export type Props = { export type Props = {

View File

@@ -1,10 +1,10 @@
@import 'src/views/styles/fonts.module.scss'; @use 'src/views/styles/fonts.module.scss';
.icon { .icon {
font-family: 'Material Icons Round'; font-family: 'Material Icons Round';
font-weight: $normal_weight; font-weight: fonts.$normal_weight;
font-style: normal; font-style: normal;
font-size: $medium_size; font-size: fonts.$medium_size;
line-height: 1; line-height: 1;
letter-spacing: normal; letter-spacing: normal;
text-transform: none; text-transform: none;

View File

@@ -1,10 +1,13 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import colors, { Color } from 'src/views/styles/colors.module.scss'; import colors, { Color } from '@views/styles/colors.module.scss';
import fonts, { Size, Weight } from 'src/views/styles/fonts.module.scss'; import fonts, { Size } from '@views/styles/fonts.module.scss';
import styles from './Icon.module.scss'; import styles from './Icon.module.scss';
import { MaterialIconCode } from './MaterialIcons'; import { MaterialIconCode } from './MaterialIcons';
/**
*
*/
export type Props = { export type Props = {
name: MaterialIconCode; name: MaterialIconCode;
className?: string; className?: string;

View File

@@ -1,6 +1,6 @@
import { background } from '@shared/messages';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { background } from 'src/shared/messages';
import Text, { TextProps } from '../Text/Text'; import Text, { TextProps } from '../Text/Text';
import styles from './Link.module.scss'; import styles from './Link.module.scss';

View File

@@ -1,4 +1,4 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
.container { .container {
width: 100%; width: 100%;
@@ -18,7 +18,7 @@
.body { .body {
overflow-y: auto; overflow-y: auto;
z-index: 2147483647; z-index: 2147483647;
background-color: $white; background-color: colors.$white;
box-shadow: 0px 12px 30px 0px #323e5f29; box-shadow: 0px 12px 30px 0px #323e5f29;
transition: box-shadow 0.15s; transition: box-shadow 0.15s;
} }

View File

@@ -1,11 +1,11 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
$spinner-border-width: 10px; $spinner-border-width: 10px;
.spinner { .spinner {
border: 1px solid $charcoal; border: 1px solid colors.$charcoal;
border-width: $spinner-border-width; border-width: $spinner-border-width;
border-top: $spinner-border-width solid $tangerine; border-top: $spinner-border-width solid colors.$tangerine;
margin: 0 auto 15px auto; margin: 0 auto 15px auto;
border-radius: 50%; border-radius: 50%;
width: 60px; width: 60px;

View File

@@ -1,59 +1,60 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
@use 'src/views/styles/fonts.module.scss';
.text { .text {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
color: $charcoal; color: colors.$charcoal;
line-height: initial; line-height: initial;
} }
.light_weight { .light_weight {
font-weight: $light_weight; font-weight: fonts.$light_weight;
} }
.regular_weight { .regular_weight {
font-weight: $regular_weight; font-weight: fonts.$regular_weight;
} }
.normal_weight { .normal_weight {
font-weight: $normal_weight; font-weight: fonts.$normal_weight;
} }
.semi_bold_weight { .semi_bold_weight {
font-weight: $semi_bold_weight; font-weight: fonts.$semi_bold_weight;
} }
.bold_weight { .bold_weight {
font-weight: $bold_weight; font-weight: fonts.$bold_weight;
} }
.black_weight { .black_weight {
font-weight: $black_weight; font-weight: fonts.$black_weight;
} }
.x_small_size { .x_small_size {
font-size: $x_small_size; font-size: fonts.$x_small_size;
} }
.xx_small_size { .xx_small_size {
font-size: $xx_small_size; font-size: fonts.$xx_small_size;
} }
.small_size { .small_size {
font-size: $small_size; font-size: fonts.$small_size;
} }
.medium_size { .medium_size {
font-size: $medium_size; font-size: fonts.$medium_size;
} }
.large_size { .large_size {
font-size: $large_size; font-size: fonts.$large_size;
} }
.x_large_size { .x_large_size {
font-size: $x_large_size; font-size: fonts.$x_large_size;
} }
.xx_large_size { .xx_large_size {
font-size: $xx_large_size; font-size: fonts.$xx_large_size;
} }

View File

@@ -1,9 +1,12 @@
import classNames from 'classnames'; import classNames from 'classnames';
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import colors, { Color } from 'src/views/styles/colors.module.scss'; import colors, { Color } from '@views/styles/colors.module.scss';
import fonts, { Size, Weight } from 'src/views/styles/fonts.module.scss'; import { Size, Weight } from '@views/styles/fonts.module.scss';
import styles from './Text.module.scss'; import styles from './Text.module.scss';
/**
*
*/
export type TextProps = { export type TextProps = {
color?: Color; color?: Color;
weight?: Weight; weight?: Weight;

View File

@@ -1,15 +1,14 @@
import { ScrapedRow } from '@shared/types/Course';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ScrapedRow } from 'src/shared/types/Course'; import useInfiniteScroll from '@views/hooks/useInfiniteScroll';
import useInfiniteScroll from 'src/views/hooks/useInfiniteScroll'; import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
import { CourseCatalogScraper } from 'src/views/lib/CourseCatalogScraper'; import { SiteSupport } from '@views/lib/getSiteSupport';
import { SiteSupport } from 'src/views/lib/getSiteSupport';
import { import {
loadNextCourseCatalogPage,
AutoLoadStatus, AutoLoadStatus,
loadNextCourseCatalogPage,
removePaginationButtons, removePaginationButtons,
} from 'src/views/lib/loadNextCourseCatalogPage'; } from '@views/lib/loadNextCourseCatalogPage';
import Spinner from '../../common/Spinner/Spinner';
import styles from './AutoLoad.module.scss'; import styles from './AutoLoad.module.scss';
type Props = { type Props = {
@@ -53,16 +52,21 @@ export default function AutoLoad({ addRows }: Props) {
addRows(scrapedRows); addRows(scrapedRows);
}, [addRows]); }, [addRows]);
if (!container || status === AutoLoadStatus.IDLE) { if (!container || status === AutoLoadStatus.DONE) {
return null; return null;
} }
return createPortal( return createPortal(
<div> <div>
{status === AutoLoadStatus.LOADING && ( {status !== AutoLoadStatus.ERROR && (
<div> <div
<Spinner /> style={{
<h2>Loading Next Page...</h2> height: '500px',
backgroundColor: '#f4f4f4',
}}
>
{/* <Spinner />
<h2>Loading Next Page...</h2> */}
</div> </div>
)} )}
{status === AutoLoadStatus.ERROR && ( {status === AutoLoadStatus.ERROR && (

View File

@@ -1,4 +1,4 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
.container { .container {
margin: 20px; margin: 20px;
@@ -22,7 +22,7 @@
} }
.restriction { .restriction {
color: $speedway_brick; color: colors.$speedway_brick;
} }
} }
} }

View File

@@ -1,10 +1,10 @@
import { Course } from '@shared/types/Course';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Course } from 'src/shared/types/Course'; import Spinner from '@views/components/common/Spinner/Spinner';
import Spinner from 'src/views/components/common/Spinner/Spinner'; import Text from '@views/components/common/Text/Text';
import Text from 'src/views/components/common/Text/Text'; import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
import { CourseCatalogScraper } from 'src/views/lib/CourseCatalogScraper'; import { SiteSupport } from '@views/lib/getSiteSupport';
import { SiteSupport } from 'src/views/lib/getSiteSupport';
import Card from '../../../common/Card/Card'; import Card from '../../../common/Card/Card';
import styles from './CourseDescription.module.scss'; import styles from './CourseDescription.module.scss';
@@ -18,6 +18,9 @@ enum LoadStatus {
ERROR = 'ERROR', ERROR = 'ERROR',
} }
/**
*
*/
export default function CourseDescription({ course }: Props) { export default function CourseDescription({ course }: Props) {
const [description, setDescription] = useState<string[]>([]); const [description, setDescription] = useState<string[]>([]);
const [status, setStatus] = useState<LoadStatus>(LoadStatus.LOADING); const [status, setStatus] = useState<LoadStatus>(LoadStatus.LOADING);

View File

@@ -1,11 +1,11 @@
import { background } from '@shared/messages';
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react'; import React from 'react';
import { background } from 'src/shared/messages'; import { Button } from '@views/components/common/Button/Button';
import { Course } from 'src/shared/types/Course'; import Card from '@views/components/common/Card/Card';
import { UserSchedule } from 'src/shared/types/UserSchedule'; import Icon from '@views/components/common/Icon/Icon';
import { Button } from 'src/views/components/common/Button/Button'; import Text from '@views/components/common/Text/Text';
import Card from 'src/views/components/common/Card/Card';
import Icon from 'src/views/components/common/Icon/Icon';
import Text from 'src/views/components/common/Text/Text';
import styles from './CourseButtons.module.scss'; import styles from './CourseButtons.module.scss';
type Props = { type Props = {

View File

@@ -1,10 +1,10 @@
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react'; import React from 'react';
import { Course } from 'src/shared/types/Course'; import Card from '@views/components/common/Card/Card';
import { UserSchedule } from 'src/shared/types/UserSchedule'; import Icon from '@views/components/common/Icon/Icon';
import Card from 'src/views/components/common/Card/Card'; import Link from '@views/components/common/Link/Link';
import Icon from 'src/views/components/common/Icon/Icon'; import Text from '@views/components/common/Text/Text';
import Link from 'src/views/components/common/Link/Link';
import Text from 'src/views/components/common/Text/Text';
import CourseButtons from './CourseButtons/CourseButtons'; import CourseButtons from './CourseButtons/CourseButtons';
import styles from './CourseHeader.module.scss'; import styles from './CourseHeader.module.scss';

View File

@@ -1,6 +1,6 @@
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react'; import React from 'react';
import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import Popup from '../../common/Popup/Popup'; import Popup from '../../common/Popup/Popup';
import CourseDescription from './CourseDescription/CourseDescription'; import CourseDescription from './CourseDescription/CourseDescription';
import CourseHeader from './CourseHeader/CourseHeader'; import CourseHeader from './CourseHeader/CourseHeader';

View File

@@ -1,4 +1,5 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
@use 'src/views/styles/elevation.module.scss';
.chartContainer { .chartContainer {
height: 250px; height: 250px;
@@ -14,11 +15,11 @@
justify-content: center; justify-content: center;
select { select {
z-index: $MAX_Z_INDEX; z-index: elevation.$MAX_Z_INDEX;
padding: 4px; padding: 4px;
font-family: 'Inter'; font-family: 'Inter';
border-radius: 8px; border-radius: 8px;
border-color: $charcoal; border-color: colors.$charcoal;
} }
} }

View File

@@ -1,19 +1,19 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import React, { useEffect, useRef, useState } from 'react'; import { Course, Semester } from '@shared/types/Course';
import HighchartsReact from 'highcharts-react-official'; import { Distribution, LetterGrade } from '@shared/types/Distribution';
import Highcharts from 'highcharts'; import Highcharts from 'highcharts';
import Card from 'src/views/components/common/Card/Card'; import HighchartsReact from 'highcharts-react-official';
import { Course, Semester } from 'src/shared/types/Course'; import React, { useEffect, useRef, useState } from 'react';
import colors from 'src/views/styles/colors.module.scss'; import Card from '@views/components/common/Card/Card';
import Spinner from 'src/views/components/common/Spinner/Spinner'; import Icon from '@views/components/common/Icon/Icon';
import Text from 'src/views/components/common/Text/Text'; import Spinner from '@views/components/common/Spinner/Spinner';
import Icon from 'src/views/components/common/Icon/Icon'; import Text from '@views/components/common/Text/Text';
import { Distribution, LetterGrade } from 'src/shared/types/Distribution';
import { import {
NoDataError, NoDataError,
queryAggregateDistribution, queryAggregateDistribution,
querySemesterDistribution, querySemesterDistribution,
} from 'src/views/lib/database/queryDistribution'; } from '@views/lib/database/queryDistribution';
import colors from '@views/styles/colors.module.scss';
import styles from './GradeDistribution.module.scss'; import styles from './GradeDistribution.module.scss';
enum DataStatus { enum DataStatus {

View File

@@ -1,7 +1,7 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
.container { .container {
background-color: $burnt_orange; background-color: colors.$burnt_orange;
color: white; color: white;
text-align: center; text-align: center;
padding: 12px; padding: 12px;

View File

@@ -15,6 +15,9 @@ export default function TableHead({ children }: PropsWithChildren) {
const lastTableHeadCell = document.querySelector('table thead th:last-child'); const lastTableHeadCell = document.querySelector('table thead th:last-child');
lastTableHeadCell!.after(container); lastTableHeadCell!.after(container);
setContainer(container); setContainer(container);
return () => {
container.remove();
};
}, []); }, []);
if (!container) { if (!container) {

View File

@@ -1,33 +1,21 @@
@import 'src/views/styles/base.module.scss'; @use 'src/views/styles/colors.module.scss';
.rowButton {
margin: 0px;
}
.selectedRow {
* {
background: $burnt_orange !important;
color: white !important;
box-shadow: none !important;
}
}
.inActiveSchedule {
* {
color: $turtle_pond !important;
font-weight: bold !important;
}
}
.isConflict {
* {
color: $speedway_brick !important;
font-weight: normal !important;
text-decoration: line-through !important;
}
}
.row { .row {
> td:first-child {
padding-left: 12px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
> td:last-child {
padding-right: 12px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
> * {
transition: background-color 0.1s ease-in-out;
}
.conflictTooltip { .conflictTooltip {
position: relative; position: relative;
display: none; display: none;
@@ -54,4 +42,46 @@
display: initial; display: initial;
} }
} }
// :global(ul.flag) li {
// transform: scale(1.5); // omg the flags are on ONE LONG GIF FILE AND SHIFTED BY Y COORDINATES
// }
}
.rowButton {
margin: 0.25rem 0;
&:active {
transform: scale(0.92);
}
width: 32px;
height: 32px;
place-items: center;
display: inline-flex;
}
.selectedRow {
> * {
background: colors.$burnt_orange !important;
color: white !important;
box-shadow: none !important;
}
.rowButton {
background: colors.$burnt_orange !important;
box-shadow: none !important;
}
}
.inActiveSchedule {
* {
color: colors.$turtle_pond !important;
font-weight: bold !important;
}
}
.isConflict {
* {
color: colors.$speedway_brick !important;
font-weight: normal !important;
text-decoration: line-through !important;
}
} }

View File

@@ -1,7 +1,7 @@
import { Course, ScrapedRow } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Course, ScrapedRow } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import { Button } from '../../common/Button/Button'; import { Button } from '../../common/Button/Button';
import Icon from '../../common/Icon/Icon'; import Icon from '../../common/Icon/Icon';
import Text from '../../common/Text/Text'; import Text from '../../common/Text/Text';
@@ -29,6 +29,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
useEffect(() => { useEffect(() => {
element.classList.add(styles.row); element.classList.add(styles.row);
const portalContainer = document.createElement('td'); const portalContainer = document.createElement('td');
portalContainer.style.textAlign = 'right';
const lastTableCell = element.querySelector('td:last-child'); const lastTableCell = element.querySelector('td:last-child');
lastTableCell!.after(portalContainer); lastTableCell!.after(portalContainer);
setContainer(portalContainer); setContainer(portalContainer);

View File

@@ -0,0 +1,9 @@
@use 'src/views/styles/fonts.module.scss';
.subheader {
h2 {
font-size: fonts.$medium_size;
margin-top: 1.25rem;
margin-bottom: 0.25rem;
}
}

View File

@@ -0,0 +1,25 @@
import { ScrapedRow } from '@shared/types/Course';
import { useEffect } from 'react';
import styles from './TableSubheading.module.scss';
interface Props {
row: ScrapedRow;
}
/**
* This component is injected into each row of the course catalog table.
* @returns a react portal to the new td in the column or null if the column has not been created yet.
*/
export default function TableSubheading({ row }: Props) {
const { element } = row;
useEffect(() => {
element.classList.add(styles.subheader);
return () => {
element.classList.remove(styles.subheader);
};
}, [element]);
return null;
}

View File

@@ -6,6 +6,9 @@ import { useEffect } from 'react';
* @returns isLoading boolean to indicate if the callback is currently being executed * @returns isLoading boolean to indicate if the callback is currently being executed
*/ */
/**
*
*/
export default function useInfiniteScroll( export default function useInfiniteScroll(
callback: () => Promise<void> | void, callback: () => Promise<void> | void,
deps?: React.DependencyList | undefined deps?: React.DependencyList | undefined
@@ -13,13 +16,15 @@ export default function useInfiniteScroll(
const isScrolling = () => { const isScrolling = () => {
const { innerHeight } = window; const { innerHeight } = window;
const { scrollTop, offsetHeight } = document.documentElement; const { scrollTop, offsetHeight } = document.documentElement;
if (innerHeight + scrollTop >= offsetHeight - 100) { if (innerHeight + scrollTop >= offsetHeight - 650) {
callback(); callback();
} }
}; };
useEffect(() => { useEffect(() => {
window.addEventListener('scroll', isScrolling); window.addEventListener('scroll', isScrolling, {
passive: true,
});
return () => window.removeEventListener('scroll', isScrolling); return () => window.removeEventListener('scroll', isScrolling);
}, deps); }, deps);
} }

View File

@@ -1,7 +1,6 @@
import { Serialized } from 'chrome-extension-toolkit'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { UserSchedule } from '@shared/types/UserSchedule';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore';
import { UserSchedule } from 'src/shared/types/UserSchedule';
export default function useSchedules(): [active: UserSchedule | null, schedules: UserSchedule[]] { export default function useSchedules(): [active: UserSchedule | null, schedules: UserSchedule[]] {
const [schedules, setSchedules] = useState<UserSchedule[]>([]); const [schedules, setSchedules] = useState<UserSchedule[]>([]);

View File

@@ -1,4 +1,4 @@
import TAB_MESSAGES from '@shared/messages/TabMessages';
import { createUseMessage } from 'chrome-extension-toolkit'; import { createUseMessage } from 'chrome-extension-toolkit';
import TAB_MESSAGES from 'src/shared/messages/TabMessages';
export const useTabMessage = createUseMessage<TAB_MESSAGES>(); export const useTabMessage = createUseMessage<TAB_MESSAGES>();

View File

@@ -1,6 +1,5 @@
import { Serialized } from 'chrome-extension-toolkit'; import { ExtensionStore } from '@shared/storage/ExtensionStore';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ExtensionStore } from 'src/shared/storage/ExtensionStore';
export default function useVersion(): string { export default function useVersion(): string {
const [version, setVersion] = useState<string>(''); const [version, setVersion] = useState<string>('');
@@ -17,4 +16,3 @@ export default function useVersion(): string {
return version; return version;
} }

View File

@@ -1,12 +1,11 @@
import { ContextInvalidated, createShadowDOM, onContextInvalidated } from 'chrome-extension-toolkit';
import React from 'react'; import React from 'react';
import { background } from 'src/shared/messages';
import render from './lib/react'; import render from './lib/react';
import { ContextInvalidated, createShadowDOM, isExtensionPopup, onContextInvalidated } from 'chrome-extension-toolkit';
import CourseCatalogMain from './components/CourseCatalogMain'; import CourseCatalogMain from './components/CourseCatalogMain';
import colors from './styles/colors.module.scss';
import getSiteSupport, { SiteSupport } from './lib/getSiteSupport';
import PopupMain from './components/PopupMain'; import PopupMain from './components/PopupMain';
import getSiteSupport, { SiteSupport } from './lib/getSiteSupport';
import colors from './styles/colors.module.scss';
const support = getSiteSupport(window.location.href); const support = getSiteSupport(window.location.href);
console.log('support:', support); console.log('support:', support);

View File

@@ -1,8 +1,7 @@
import { Serialized } from 'chrome-extension-toolkit'; import { Course, InstructionMode, ScrapedRow, Semester, Status } from '@shared/types/Course';
import { Course, Status, InstructionMode, ScrapedRow, Semester } from 'src/shared/types/Course'; import { CourseSchedule } from '@shared/types/CourseSchedule';
import { CourseSchedule } from 'src/shared/types/CourseSchedule'; import Instructor from '@shared/types/Instructor';
import Instructor from 'src/shared/types/Instructor'; import { SiteSupport } from '@views/lib/getSiteSupport';
import { SiteSupport } from 'src/views/lib/getSiteSupport';
/** /**
* The selectors that we use to scrape the course catalog list table (https://utdirect.utexas.edu/apps/registrar/course_schedule/20239/results/?fos_fl=C+S&level=U&search_type_main=FIELD) * The selectors that we use to scrape the course catalog list table (https://utdirect.utexas.edu/apps/registrar/course_schedule/20239/results/?fos_fl=C+S&level=U&search_type_main=FIELD)

View File

@@ -1,7 +1,8 @@
import initSqlJs from 'sql.js/dist/sql-wasm'; import initSqlJs from 'sql.js/dist/sql-wasm';
const WASM_FILE_URL = chrome.runtime.getURL('database/sql-wasm.wasm'); import DB_FILE_URL from '@public/database/grades.db?url';
const DB_FILE_URL = chrome.runtime.getURL('database/grades.db'); import WASM_FILE_URL from 'sql.js/dist/sql-wasm.wasm?url';
// import WASM_FILE_URL from '../../../../public/database/sql-wasm.wasm?url';
/** /**
* A utility type for the SQL.js Database type * A utility type for the SQL.js Database type

View File

@@ -1,5 +1,5 @@
import { Course, Semester } from 'src/shared/types/Course'; import { Course, Semester } from '@shared/types/Course';
import { CourseSQLRow, Distribution } from 'src/shared/types/Distribution'; import { CourseSQLRow, Distribution } from '@shared/types/Distribution';
import { initializeDB } from './initializeDB'; import { initializeDB } from './initializeDB';
/** /**

View File

@@ -9,6 +9,7 @@ export enum AutoLoadStatus {
LOADING = 'LOADING', LOADING = 'LOADING',
IDLE = 'IDLE', IDLE = 'IDLE',
ERROR = 'ERROR', ERROR = 'ERROR',
DONE = 'DONE',
} }
let isLoading = false; let isLoading = false;
@@ -24,7 +25,7 @@ let nextPageURL = getNextButton(document)?.href;
export async function loadNextCourseCatalogPage(): Promise<[AutoLoadStatus, HTMLTableRowElement[]]> { export async function loadNextCourseCatalogPage(): Promise<[AutoLoadStatus, HTMLTableRowElement[]]> {
// if there is no more nextPageURL, then we have reached the end of the course catalog, so we can stop // if there is no more nextPageURL, then we have reached the end of the course catalog, so we can stop
if (!nextPageURL) { if (!nextPageURL) {
return [AutoLoadStatus.IDLE, []]; return [AutoLoadStatus.DONE, []];
} }
// remove the next button so that we don't load the same page twice // remove the next button so that we don't load the same page twice
removePaginationButtons(document); removePaginationButtons(document);

View File

@@ -1,4 +1,4 @@
@import './colors.module.scss'; @use 'colors.module.scss';
@import './fonts.module.scss'; @use 'fonts.module.scss';
@import './elevation.module.scss'; @use 'elevation.module.scss';
@import './utils.module.scss'; @use 'utils.module.scss';

View File

@@ -1,7 +1,7 @@
@each $weights in '100' '200' '300' '400' '500' '600' '700' '800' '900' { @each $weights in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
src: url('chrome-extension://__MSG_@@extension_id__/fonts/inter-#{$weights}.woff2') format('woff2'); src: url('@public/fonts/inter-#{$weights}.woff2') format('woff2');
font-display: auto; font-display: auto;
font-style: normal; font-style: normal;
font-weight: #{$weights}; font-weight: #{$weights};
@@ -13,7 +13,7 @@
font-style: normal; font-style: normal;
font-display: block; font-display: block;
font-weight: 400; font-weight: 400;
src: url('chrome-extension://__MSG_@@extension_id__/fonts/material-icons.woff2') format('woff2'); src: url('@public/fonts/material-icons.woff2') format('woff2');
} }
$light_weight: 300; $light_weight: 300;

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -1,55 +1,57 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2021", "baseUrl": ".",
"outDir": "./", "rootDir": ".",
"target": "esnext",
"module": "esnext",
"noEmit": true, "noEmit": true,
"jsx": "react",
"typeRoots": [ "typeRoots": [
"./node_modules/@types", "./node_modules/@types",
"./@types/" "./@types/"
], ],
"rootDir": "./",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"module": "ES2022",
"moduleResolution": "node",
"allowJs": true,
"sourceMap": true,
"resolveJsonModule": true,
"incremental": true,
"lib": [
"DOM",
"es2021"
],
"jsx": "react",
"skipLibCheck": true, "skipLibCheck": true,
"strictBindCallApply": true, "esModuleInterop": true,
"pretty": true, "resolveJsonModule": true,
"noImplicitReturns": false, "moduleResolution": "node",
"baseUrl": "./", "types": [
"vite/client",
"node"
],
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"paths": { "paths": {
"src/*": [ "src/*": [
"./src/*" "src/*"
], ],
"webpack/*": [ "@assets/*": [
"./webpack/*" "src/assets/*"
], ],
}, "@pages/*": [
"noImplicitThis": true, "src/pages/*"
"noImplicitAny": false, ],
"strictNullChecks": true, "@public/*": [
"forceConsistentCasingInFileNames": true, "public/*"
],
"@shared/*": [
"src/shared/*"
],
"@background/*": [
"src/pages/background/*"
],
"@views/*": [
"src/views/*"
],
}
}, },
"include": [ "include": [
"src/**/*", "src",
"webpack/**/*", "utils",
"@types/**/*", "vite.config.ts",
"./package.json", "node_modules/@types",
"./release.config.js", "src/manifest.ts",
"webpack/plugins/custom/.ts" "package.json",
], ".eslintrc",
"exclude": [ "postcss.config.cjs"
"node_modules", ]
"**/.*/",
"build",
],
} }

48
utils/log.ts Normal file
View File

@@ -0,0 +1,48 @@
type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS;
export default function colorLog(message: string, type?: ColorType) {
let color: string = type || COLORS.FgBlack;
switch (type) {
case 'success':
color = COLORS.FgGreen;
break;
case 'info':
color = COLORS.FgBlue;
break;
case 'error':
color = COLORS.FgRed;
break;
case 'warning':
color = COLORS.FgYellow;
break;
}
console.log(color, message);
}
const COLORS = {
Reset: '\x1b[0m',
Bright: '\x1b[1m',
Dim: '\x1b[2m',
Underscore: '\x1b[4m',
Blink: '\x1b[5m',
Reverse: '\x1b[7m',
Hidden: '\x1b[8m',
FgBlack: '\x1b[30m',
FgRed: '\x1b[31m',
FgGreen: '\x1b[32m',
FgYellow: '\x1b[33m',
FgBlue: '\x1b[34m',
FgMagenta: '\x1b[35m',
FgCyan: '\x1b[36m',
FgWhite: '\x1b[37m',
BgBlack: '\x1b[40m',
BgRed: '\x1b[41m',
BgGreen: '\x1b[42m',
BgYellow: '\x1b[43m',
BgBlue: '\x1b[44m',
BgMagenta: '\x1b[45m',
BgCyan: '\x1b[46m',
BgWhite: '\x1b[47m',
} as const;

View File

@@ -0,0 +1,26 @@
import * as fs from 'fs';
import * as path from 'path';
import { PluginOption } from 'vite';
import manifest from '../../src/manifest';
import colorLog from '../log';
const { resolve } = path;
const outDir = resolve(__dirname, '..', '..', 'public');
export default function makeManifest(): PluginOption {
return {
name: 'make-manifest',
buildEnd() {
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir);
}
const manifestPath = resolve(outDir, 'manifest.json');
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
colorLog(`Manifest file copy complete: ${manifestPath}`, 'success');
},
};
}

148
vite.config.ts Normal file
View File

@@ -0,0 +1,148 @@
import { crx } from '@crxjs/vite-plugin';
import react from '@vitejs/plugin-react-swc';
import { resolve } from 'path';
import { Plugin, ResolvedConfig, ViteDevServer, defineConfig } from 'vite';
import inspect from 'vite-plugin-inspect';
import manifest from './src/manifest';
const root = resolve(__dirname, 'src');
const pagesDir = resolve(root, 'pages');
const assetsDir = resolve(root, 'assets');
const outDir = resolve(__dirname, 'dist');
const publicDir = resolve(__dirname, 'public');
const isDev = process.env.NODE_ENV === 'development';
export const preambleCode = `
import RefreshRuntime from "__BASE__@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
`;
const renameFile = (source: string, destination: string): Plugin => {
if (typeof source !== 'string' || typeof destination !== 'string') {
return;
}
return {
name: 'crx:rename-file',
apply: 'build',
enforce: 'post',
generateBundle(options, bundle) {
if (!bundle[source]) return;
bundle[source].fileName = destination;
},
};
};
let config: ResolvedConfig;
let server: ViteDevServer;
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
// crx({ manifest, contentScripts: { preambleCode } }),
crx({ manifest }),
inspect(),
{
name: 'public-transform',
apply: 'serve',
transform(code, id) {
if (id.endsWith('.tsx') || id.endsWith('.ts') || id.endsWith('?url')) {
return code.replace(
/(['"])(\/public\/.*?)(['"])/g,
(_, quote1, path, quote2) => `chrome.runtime.getURL(${quote1}${path}${quote2})`
);
}
},
},
{
name: 'public-transform',
apply: 'build',
transform(code, id) {
if (id.endsWith('.tsx') || id.endsWith('.ts') || id.endsWith('?url')) {
return code.replace(
/(['"])(__VITE_ASSET__.*?__)(['"])/g,
(_, quote1, path, quote2) => `chrome.runtime.getURL(${quote1}${path}${quote2})`
);
}
},
},
{
name: 'public-css-dev-transform',
apply: 'serve',
enforce: 'post',
transform(code, id) {
if (process.env.NODE_ENV === 'development' && (id.endsWith('.css') || id.endsWith('.scss'))) {
return code.replace(
/url\((.*?)\)/g,
(_, path) =>
`url(\\"" + chrome.runtime.getURL(${path
.replaceAll(`\\"`, `"`)
.replace(/public\//, '')}) + "\\")`
);
}
},
},
{
name: 'public-transform2',
// enforce: 'post',
transform(code, id) {
if (id.replace(/\?used$/, '').endsWith('.scss')) {
const transformedCode = code.replace(
/(__VITE_ASSET__.*?__)/g,
(_, path) => `chrome-extension://__MSG_@@extension_id__${path}`
);
return transformedCode;
}
return code;
},
},
// renameFile('src/pages/debug/index.html', 'debug.html'),
renameFile('src/pages/calendar/index.html', 'calendar.html'),
],
resolve: {
alias: {
src: root,
'@assets': assetsDir,
'@pages': pagesDir,
'@public': publicDir,
'@shared': resolve(root, 'shared'),
'@background': resolve(pagesDir, 'background'),
'@views': resolve(root, 'views'),
},
},
server: {
strictPort: true,
port: 5173,
hmr: {
clientPort: 5173,
},
proxy: {
'/debug.html': {
target: 'http://localhost:5173',
rewrite: path => path.replace('debug', 'src/pages/debug/index'),
},
'/calendar.html': {
target: 'http://localhost:5173',
rewrite: path => path.replace('calendar', 'src/pages/calendar/index'),
},
},
},
build: {
rollupOptions: {
input: {
debug: 'src/pages/debug/index.html',
calendar: 'src/pages/calendar/index.html',
},
// output: {
// entryFileNames: `[name].js`, // otherwise it will add the hash
// chunkFileNames: `[name].js`,
// },
// external: ['/@react-refresh'],
},
},
});

View File

@@ -1,41 +0,0 @@
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import path from 'path';
import { Server } from 'socket.io';
import config from './webpack.config';
import { version } from '../package.json';
import { getManifest } from './manifest.config';
import { initializeHotReloading } from './plugins/custom/hotReloadServer';
const HOT_RELOAD_PORT = 9090;
const MODE: Environment = 'development';
const manifest = getManifest(MODE, version);
const compiler = webpack(config(MODE, manifest));
initializeHotReloading(HOT_RELOAD_PORT, compiler);
const server = new WebpackDevServer(
{
https: false,
hot: false,
client: false,
host: 'localhost',
static: {
directory: path.resolve('build'),
},
devMiddleware: {
writeToDisk: true,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
allowedHosts: 'all',
watchFiles: {
paths: ['src/**/*.{ts,tsx,js,jsx,html,css,scss,json,md,png,jpg,jpeg,gif,svg}', 'public/**/*'],
},
},
compiler
);
await server.start();

View File

@@ -1,52 +0,0 @@
import { RuleSetRule } from 'webpack';
import * as styleLoaders from './styleLoaders';
/** using esbuild-loader for ⚡ fast builds */
const typescriptLoader: RuleSetRule = {
test: /\.tsx?$/,
loader: 'esbuild-loader',
options: {
loader: 'tsx',
target: 'es2021',
},
};
/** convert svgs to react components automatically */
const svgLoader: RuleSetRule = {
test: /\.svg$/,
issuer: /\.tsx?$/,
loader: '@svgr/webpack',
};
/** these are files that we want to be able to be loaded into the extension folder instead of imported */
const urlLoader: RuleSetRule = {
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.mp3$/],
loader: 'url-loader',
options: {
limit: '10000',
name: 'static/media/[name].[ext]',
},
};
/** these loaders will allow us to use raw css imports, css modules, raw sass imports, and sass modules */
const { cssLoader, cssModuleLoader, sassLoader, sassModuleLoader } = styleLoaders;
// this is the default file loader, it will be used for any file that doesn't match the other loaders
const fileLoader: RuleSetRule = {
loader: 'file-loader',
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.mp3$/],
options: {
name: 'static/media/[name].[ext]',
},
};
/** the assembled list of loaders in the order that we want webpack to attempt to use them on modules */
const loaders: RuleSetRule[] = [
typescriptLoader,
{
// IMPORTANT: if you are adding a new loader, it must come before the file loader
oneOf: [svgLoader, urlLoader, cssLoader, cssModuleLoader, sassLoader, sassModuleLoader, fileLoader],
},
];
export default loaders;

View File

@@ -1,75 +0,0 @@
import { RuleSetRule, RuleSetUseItem } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import getCSSModuleLocalIdent from 'react-dev-utils/getCSSModuleLocalIdent';
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
function buildStyleLoaders(cssLoaderOptions: Record<string, any>): RuleSetUseItem[] {
const loaders = [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: 'css-loader',
options: { ...cssLoaderOptions, sourceMap: false },
},
];
return loaders;
}
export const cssLoader: RuleSetRule = {
test: cssRegex,
exclude: cssModuleRegex,
sideEffects: true,
use: [
...buildStyleLoaders({
importLoaders: 1,
esModule: false,
}),
],
};
export const cssModuleLoader: RuleSetRule = {
test: cssModuleRegex,
use: [
...buildStyleLoaders({
importLoaders: 1,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
],
};
export const sassLoader: RuleSetRule = {
test: sassRegex,
exclude: sassModuleRegex,
sideEffects: true,
use: [
...buildStyleLoaders({
importLoaders: 2,
}),
{
loader: 'sass-loader',
},
],
};
export const sassModuleLoader: RuleSetRule = {
test: sassModuleRegex,
use: [
...buildStyleLoaders({
importLoaders: 2,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
{
loader: 'sass-loader',
},
],
};

View File

@@ -1,62 +0,0 @@
const NAME = 'UT Registration Plus';
const SHORT_NAME = 'ut-registration-plus';
const DESCRIPTION = 'Improves the course registration process at the University of Texas at Austin!';
const HOST_PERMISSIONS: string[] = [
'*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*',
'*://*.utexas.collegescheduler.com/*',
'*://*.catalog.utexas.edu/ribbit/',
'*://*.registrar.utexas.edu/schedules/*',
'*://*.login.utexas.edu/login/*',
];
/**
* Creates a chrome extension manifest from the given version, mode, and
* @param mode the build mode (development or production)
* @param version a chrome extension version (not a semantic version)
* @returns a chrome extension manifest
*/
export function getManifest(mode: Environment, version: string): chrome.runtime.ManifestV3 {
let name = mode === 'development' ? `${NAME} (dev)` : NAME;
if (mode === 'development') {
HOST_PERMISSIONS.push('http://localhost:9090/*');
}
const manifest = {
name,
short_name: SHORT_NAME,
description: DESCRIPTION,
version,
manifest_version: 3,
// hardcode the key for development builds
key: process.env.MANIFEST_KEY,
host_permissions: HOST_PERMISSIONS,
permissions: ['storage', 'unlimitedStorage', 'background'],
background: {
service_worker: 'static/js/background.js',
},
content_scripts: [
{
matches: HOST_PERMISSIONS,
css: ['/static/css/content.css'],
js: ['/static/js/content.js'],
},
],
web_accessible_resources: [
{
resources: ['static/media/*', '*'],
matches: ['<all_urls>'],
},
],
icons: {
16: `icons/icon_${mode}_16.png`,
48: `icons/icon_${mode}_48.png`,
128: `icons/icon_${mode}_128.png`,
},
action: {
default_popup: 'popup.html',
},
} satisfies chrome.runtime.ManifestV3;
return manifest;
}

View File

@@ -1,120 +0,0 @@
import path from 'path';
import dotenv from 'dotenv';
import webpack, { WebpackPluginInstance } from 'webpack';
import { EntryId } from 'webpack/webpack.config';
import CreateFileWebpack from 'create-file-webpack';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import WebpackBuildNotifierPlugin from 'webpack-build-notifier';
import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin';
import HTMLWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import TypeErrorNotifierPlugin from './custom/TypeErrorNotifierPlugin';
/**
* Gets the plugins that are used in the build process
* @param mode the environment that the build is running in
* @param htmlEntries the entry points that need an html file
* @param manifest the manifest.json file
* @returns an array of webpack plugins
*/
export function getBuildPlugins(mode: Environment, htmlEntries: EntryId[], manifest: chrome.runtime.ManifestV3) {
let plugins: WebpackPluginInstance[] = [];
// show the progress of the build
plugins.push(new webpack.ProgressPlugin());
// make sure that the paths are case sensitive
plugins.push(new CaseSensitivePathsPlugin());
plugins.push(new CleanWebpackPlugin());
// specify how the outputed css files should be named
plugins.push(
new MiniCssExtractPlugin({
filename: 'static/css/[name].css',
chunkFilename: 'static/css/[name].chunk.css',
})
);
// create an html file for each entry point that needs one
for (const entryId of htmlEntries) {
// if (!entries[entryId]) return;
plugins.push(
new HTMLWebpackPlugin({
hash: false,
filename: `${entryId}.html`,
chunks: [entryId],
title: `${entryId} `,
template: path.resolve('webpack', 'plugins', 'template.html'),
})
);
}
// write the manifest.json file to the build directory
plugins.push(
new CreateFileWebpack({
path: path.resolve('build'),
fileName: 'manifest.json',
content: JSON.stringify(manifest, null, 2),
})
);
// copy the public directory to the build directory, but only copy the icons for the current mode
plugins.push(
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve('public'),
filter: path => (path.includes('icons/icon') ? path.includes(mode) : true),
},
],
})
);
// run the typescript checker in a separate process
plugins.push(
new ForkTsCheckerWebpackPlugin({
async: false,
})
);
// notify the developer of build events when in development mode
if (mode === 'development') {
plugins.push(
new WebpackBuildNotifierPlugin({
title: `${manifest.short_name} v${manifest.version} ${mode}`,
logo: path.resolve('public', 'icons', 'icon_production_128.png'),
failureSound: 'Ping',
successSound: false,
showDuration: true,
suppressWarning: true,
})
);
}
// notify the developer of type errors
plugins.push(new TypeErrorNotifierPlugin());
// define the environment variables that are available within the extension code
plugins.push(
new webpack.DefinePlugin({
'process.env': JSON.stringify({
SEMANTIC_VERSION: process.env.SEMANTIC_VERSION,
NODE_ENV: mode,
...dotenv.config({ path: `.env.${mode}` }).parsed,
} satisfies typeof process.env),
})
);
// provide some global nodejs variables so that nodejs libraries can be used
plugins.push(
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
process: 'process/browser',
})
);
return plugins;
}

View File

@@ -1,84 +0,0 @@
import { Compiler } from 'webpack';
import path from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import WebpackBuildNotifierPlugin from 'webpack-build-notifier';
import { Issue, IssueLocation } from 'fork-ts-checker-webpack-plugin/lib/issue';
interface Resource {
path: string;
location: IssueLocation;
}
/**
* This plugin hooks into the fork-ts-checker-webpack-plugin and
* notifies the developer of type errors using the webpack-build-notifier plugin.
*/
export default class TypeErrorNotifierPlugin {
apply(compiler: Compiler) {
// hook into the fork-ts-checker-webpack-plugin
const hooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler);
hooks.issues.tap('MyPlugin', issues => {
const errors = issues.filter(issue => issue.severity === 'error');
if (!errors?.[0]?.message) {
return errors;
}
let error = errors[0];
let resource = getErrorResource(error);
try {
notifyTypeError(resource, error.message, errors);
} catch (e) {
console.error(e);
}
return errors;
});
}
}
function notifyTypeError(resource: Resource, message: string, errors: Issue[]) {
const { line, column } = resource.location.start;
const buildNotifier = new WebpackBuildNotifierPlugin({
logo: path.resolve('public', 'icons', 'icon_production_128.png'),
compilationSound: 'Pop',
failureSound: 'Sosumi',
title: `TS: ${errors.length} errors`,
notifyOptions: {
open: `vscode://file/${resource.path}:${line}:${column}`,
},
});
const fakeInput = {
hasErrors: () => true,
compilation: {
children: null,
errors: [
{
message,
module: {
resource: resource.path,
},
},
],
},
};
// @ts-ignore - private method
buildNotifier.onCompilationDone(fakeInput);
}
function getErrorResource(error: Issue): Resource {
return {
path: error.file ?? '',
location: error.location ?? {
end: {
column: 0,
line: 0,
},
start: {
column: 0,
line: 0,
},
},
};
}

Some files were not shown because too many files have changed in this diff Show More