Refactor extension state and UI surfaces - #1
Conversation
|
There was a problem hiding this comment.
Code Review
This pull request transitions the project from 'Tiny Helmet' to 'CRXKit', introducing a config-driven architecture powered by a new extension.config.json file and a project-local CLI for validation, scaffolding, and packaging. The codebase has been refactored to support this structure, alongside the addition of Vitest unit tests and Playwright E2E tests. Feedback on the changes highlights a potential race condition in the background script's settings hydration, robustness issues in CLI argument parsing when flags precede positional arguments, a redundant ternary operator in the CLI script, and unsafe nested property access in the options page.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let cachedSettings = copySettings(config.defaultSettings); | ||
| let hydrating = false; | ||
|
|
||
| async function hydrateSettings() { | ||
| if (hydrating) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| hydrating = true; | ||
| cachedSettings = copySettings(await loadSettings()); | ||
| } finally { | ||
| hydrating = false; | ||
| } | ||
| } |
There was a problem hiding this comment.
There is a potential race condition in hydrateSettings. If multiple events (like tab updates or activations) trigger hydration concurrently, subsequent callers will return undefined immediately while hydrating is true, proceeding to syncSidePanel before the settings have actually finished loading. Using a promise-coalescing pattern resolves this issue by returning the active hydration promise to all concurrent callers.
let cachedSettings = copySettings(config.defaultSettings);
let hydrationPromise: Promise<void> | null = null;
function hydrateSettings(): Promise<void> {
if (hydrationPromise) {
return hydrationPromise;
}
hydrationPromise = (async () => {
try {
cachedSettings = copySettings(await loadSettings());
} finally {
hydrationPromise = null;
}
})();
return hydrationPromise;
}|
|
||
| const entryDir = path.join('src/entries', name); | ||
| const input = `${entryDir}/main.${PAGE_KINDS.has(kind) ? 'tsx' : 'ts'}`; | ||
| const output = kind === 'background' ? `${name}.js` : `${name}.js`; |
| async function addEntry(cwd, args, io) { | ||
| const name = args[0]; | ||
| const { flags } = parseFlags(args.slice(1)); | ||
| const kind = flags.kind; |
There was a problem hiding this comment.
Extracting the positional argument directly from args[0] before parsing flags assumes that the name is always the first argument. If a user passes flags before the name (e.g., crx entry add --kind page demo), the flag will be incorrectly treated as the entry name. Parsing the entire args array first is more robust.
| async function addEntry(cwd, args, io) { | |
| const name = args[0]; | |
| const { flags } = parseFlags(args.slice(1)); | |
| const kind = flags.kind; | |
| async function addEntry(cwd, args, io) { | |
| const { flags, positional } = parseFlags(args); | |
| const name = positional[0]; | |
| const kind = flags.kind; |
| async function createProject(cwd, args, io) { | ||
| const target = args[0]; | ||
| const { flags } = parseFlags(args.slice(1)); |
There was a problem hiding this comment.
Extracting the target directory directly from args[0] before parsing flags assumes that the target is always the first argument. If a user passes flags before the target (e.g., crx create --package com.example my-app), the flag will be incorrectly treated as the target directory. Parsing the entire args array first is more robust.
| async function createProject(cwd, args, io) { | |
| const target = args[0]; | |
| const { flags } = parseFlags(args.slice(1)); | |
| async function createProject(cwd, args, io) { | |
| const { flags, positional } = parseFlags(args); | |
| const target = positional[0]; |
| <button | ||
| type="button" | ||
| key={section.id} | ||
| onClick={() => setActiveTab(section.id)} | ||
| onClick={() => setSidePanelAutoOpen(!settings.sidePanel.autoOpen)} | ||
| className={clsx( | ||
| 'flex w-full items-center gap-3 rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200', | ||
| activeTab === section.id | ||
| ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 scale-[1.02]' | ||
| : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground' | ||
| 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors', | ||
| settings.sidePanel.autoOpen ? 'bg-primary' : 'bg-muted' | ||
| )} | ||
| aria-pressed={settings.sidePanel.autoOpen} | ||
| > | ||
| <section.icon className="h-4 w-4" /> | ||
| {section.label} | ||
| <span | ||
| className={clsx( | ||
| 'block h-5 w-5 rounded-full bg-background shadow transition-transform', | ||
| settings.sidePanel.autoOpen ? 'translate-x-5' : 'translate-x-1' | ||
| )} | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
Directly accessing nested properties on settings.sidePanel without optional chaining or fallback guards can cause runtime crashes if the settings object is partially hydrated or if backward compatibility issues arise (e.g., missing the sidePanel property). Adding optional chaining and a fallback value ensures defensive programming.
| <button | |
| type="button" | |
| key={section.id} | |
| onClick={() => setActiveTab(section.id)} | |
| onClick={() => setSidePanelAutoOpen(!settings.sidePanel.autoOpen)} | |
| className={clsx( | |
| 'flex w-full items-center gap-3 rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200', | |
| activeTab === section.id | |
| ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 scale-[1.02]' | |
| : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground' | |
| 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors', | |
| settings.sidePanel.autoOpen ? 'bg-primary' : 'bg-muted' | |
| )} | |
| aria-pressed={settings.sidePanel.autoOpen} | |
| > | |
| <section.icon className="h-4 w-4" /> | |
| {section.label} | |
| <span | |
| className={clsx( | |
| 'block h-5 w-5 rounded-full bg-background shadow transition-transform', | |
| settings.sidePanel.autoOpen ? 'translate-x-5' : 'translate-x-1' | |
| )} | |
| /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setSidePanelAutoOpen(!(settings.sidePanel?.autoOpen ?? true))} | |
| className={clsx( | |
| 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors', | |
| (settings.sidePanel?.autoOpen ?? true) ? 'bg-primary' : 'bg-muted' | |
| )} | |
| aria-pressed={settings.sidePanel?.autoOpen ?? true} | |
| > | |
| <span | |
| className={clsx( | |
| 'block h-5 w-5 rounded-full bg-background shadow transition-transform', | |
| (settings.sidePanel?.autoOpen ?? true) ? 'translate-x-5' : 'translate-x-1' | |
| )} | |
| /> | |
| </button> |
No description provided.