Skip to content

build: Enable live reloading using web-ext, esbuild, and chokidar#738

Merged
dvanoni merged 14 commits intomainfrom
live-reload
Sep 28, 2025
Merged

build: Enable live reloading using web-ext, esbuild, and chokidar#738
dvanoni merged 14 commits intomainfrom
live-reload

Conversation

@dvanoni
Copy link
Copy Markdown
Owner

@dvanoni dvanoni commented Sep 22, 2025

Summary by CodeRabbit

  • Documentation

    • README updated: optional dev start, watch-enabled builds, web-ext-based startup with automatic reload, and configurable debug log file.
  • Chores

    • Live rebuild and asset watching enabled; dynamic version/update/manifest handling introduced; added Node engine requirement and new dev tooling; removed legacy prestart/poststart steps; simplified example config; tightened .gitignore patterns.
  • Tests

    • No user-facing changes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 22, 2025

Walkthrough

Consolidates and modernizes build/start tooling: introduces watchable esbuild contexts and asset watcher, replaces child_process launcher with a web-ext-based extension runner, moves manifest/version logic to async utils, updates path roots and types, and adjusts README/scripts and .gitignore entries.

Changes

Cohort / File(s) Summary
Repo config & docs
/.gitignore, README.md, package.json, zotero.config.example.json
Normalize root ignore patterns; document new watch/start flow and add start:dev; add web-ext and chokidar devDependencies, build:watch script, remove prestart; remove profile.name and top-level scripts from example config.
Unified watchable build
/scripts/build.mts
Replace per-entry esbuild invocations with unified esbuild contexts (bootstrap + content), add watch mode, plugin wiring, asset/manifest hooks, and provide lifecycle cleanup.
Start flow (web-ext)
/scripts/start.mts, /types/web-ext.d.ts
Replace child_process-based launcher with async web-ext runner, integrate build watcher and optional log streaming, validate manifest at runtime; add TS declarations for web-ext API.
Asset utilities
/scripts/utils/copy-assets.mts
New asset copy/watch module exposing copyAssets() and copyAndWatchAssets() with ignore patterns and a CleanupFunction for watch mode.
Install manifest utility
/scripts/utils/generate-install-manifest.mts, (removed) /scripts/generate-install-manifest.mts
Remove old standalone generator; add async generateInstallManifest() util that writes buildDir/manifest.json.
Versioning & update manifest
/scripts/utils/version.mts, /scripts/create-xpi.mts, /scripts/generate-update-manifest.mts
Introduce async getVersion() (lazy read/write of version.json), update consumers to await version, adjust import paths to ./utils/*, and include dynamic version in updates.json/XPI naming.
Paths & generators
/scripts/utils/paths.mts, /scripts/generate-fluent-types.mts
Shift rootDir up one level (../..) affecting buildDir/genDir resolution and update generator imports to ./utils/paths.mts; fluent types generation unchanged.
Types
/types/web-ext.d.ts
Add TypeScript declarations for web-ext API (commands, options, runners, results).
Removed script
/scripts/generate-install-manifest.mts
Deleted the standalone manifest generation script; functionality moved to utils.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Dev as Developer
  participant Build as build() (esbuild ctx)
  participant Assets as copyAndWatchAssets()
  participant GenMan as generateInstallManifest()
  participant WebExt as web-ext runner
  participant Zotero as Zotero App

  Dev->>Build: invoke build({sourcemap, watch})
  Build->>Assets: copyAndWatchAssets()/copyAssets()
  Assets-->>Build: assets ready / watcher returned
  Build->>GenMan: generateInstallManifest()
  GenMan-->>Build: manifest.json written
  Build-->>Dev: contexts watching / CleanupFunction
  Dev->>WebExt: run(sourceDir=buildDir, options)
  WebExt->>Zotero: launch with profile & args
  Note over Build,WebExt: on rebuild -> WebExt.reload()
Loading
sequenceDiagram
  autonumber
  participant Script as Script
  participant Ver as getVersion()
  participant FS as version.json
  participant Pkg as package.json

  Script->>Ver: await getVersion()
  alt version.json exists
    Ver->>FS: read version.json
    FS-->>Ver: version
  else
    Ver->>Pkg: read package.json metadata
    Ver->>Ver: computeVersion()
    Ver->>FS: write version.json
  end
  Ver-->>Script: version string
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

maintenance

Poem

I thump the keys and watch the tree—
Builds that live and reload with me.
Manifests stitched and versions stored,
Web‑ext wakes Zotero, logs restored.
A rabbit hops — dev joy, deploy! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes the primary purpose of the changeset—enabling live reloading—and names the key tools (web-ext, esbuild, chokidar) involved, making it clear to reviewers what the main feature is.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch live-reload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Sep 22, 2025

🤖 Build Bot

✅ Successful build

⬇️ Download: notero-1.2.2-852

@socket-security
Copy link
Copy Markdown

socket-security bot commented Sep 22, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedchokidar@​4.0.39910010080100
Addedweb-ext@​8.10.0961001008970

View full report

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
scripts/generate-fluent-types.mts (1)

23-27: import.meta.filename is Node 22‑only; switch to fileURLToPath

Prevents runtime errors on Node 18/20 and Windows.

+import { fileURLToPath } from 'node:url';
 ...
-const tsContent = `// This file is generated by ${relativeToRoot(import.meta.filename)}
+const thisFile = fileURLToPath(import.meta.url);
+const tsContent = `// This file is generated by ${relativeToRoot(thisFile)}
 
 export type FluentMessageId =
 ${messageIds.map((id) => `  | '${id}'`).join('\n')};
 `;
scripts/create-xpi.mts (1)

19-21: Ensure output directory exists before writing XPI

Avoids ENOENT if /xpi doesn’t exist on clean clones/CI.

 const zip = new AdmZip();
 zip.addLocalFolder(buildDir);
+fs.ensureDirSync(xpiDir);
 zip.writeZip(xpiPath);
🧹 Nitpick comments (12)
.gitignore (1)

1-7: Root‑anchored ignores look good; also ignore the xpi output dir

Since artifacts are written to /xpi, ignore the directory to avoid empty dir churn.

 /node_modules
 /src/locale/fluent-types.ts
 /zotero.config.json
 /.env
 /.eslintcache
+/xpi
scripts/utils/version.mts (2)

46-48: Guard against missing GITHUB_RUN_NUMBER

Avoids producing versions like x.y.z-undefined.

-function getPrereleaseVersion(baseVersion: string): string {
-  return `${baseVersion}-${process.env.GITHUB_RUN_NUMBER}`;
-}
+function getPrereleaseVersion(baseVersion: string): string {
+  const run = process.env.GITHUB_RUN_NUMBER ?? '0';
+  return `${baseVersion}-${run}`;
+}

42-44: os.userInfo() may throw in containers; add a safe fallback

Improves robustness in CI and minimal container images.

-function getLocalVersion(): string {
-  return `${getPatchBumpVersion()}-${os.userInfo().username}.${os.hostname()}`;
-}
+function getLocalVersion(): string {
+  let user = 'unknown';
+  try {
+    user = os.userInfo().username;
+  } catch {}
+  return `${getPatchBumpVersion()}-${user}.${os.hostname()}`;
+}
package.json (1)

94-96: web-ext@8.10.0 is compatible with CI Node; TS types are present — consider adding package.json "engines"

  • web-ext (^8.10.0) resolves to 8.10.0 (package-lock.json); web-ext lists engines.node ">=18.0.0".
  • CI uses .node-version = 24.3.0 and actions/setup-node@v4 with node-version-file: '.node-version' (.github/workflows/build.yml, release.yml) — CI Node satisfies web-ext requirement.
  • scripts/start.mts imports from 'web-ext' and types/web-ext.d.ts is present — TypeScript typings available.
  • package.json has no "engines" field; optional: add "engines": { "node": ">=18" } to document/enforce runtime (or keep relying on .node-version + workflows).
scripts/utils/copy-assets.mts (2)

7-8: tests not fully ignored; nested files still copied.

Your regex only matches trailing “…/tests” or “.DS_Store”, so files inside “tests” are included. Tighten the pattern to ignore the directory anywhere in the path (both POSIX and Windows).

Apply this diff:

-const IGNORED_PATHS = /(\.DS_Store|__tests__)$/;
+const IGNORED_PATHS = /(?:^|[\\/])(?:\.DS_Store|__tests__)(?:[\\/]|$)/;

Also applies to: 31-41


28-46: Asset changes don’t trigger rebuild/reload.

copyAssets() is only invoked once before esbuild watch starts. Changes to non‑TS(X)/JSON assets won’t be recopied, so web‑ext won’t reload. Consider watching assets and re‑running copyAssets() on change.

I can sketch a chokidar watcher wired into the build flow if you want.

README.md (1)

412-416: Add minimum Node version requirement near start instructions.

Docs now depend on web‑ext and ESM features (TLA, JSON import attributes). Add a brief note (e.g., “Requires Node ≥ 20.x”) to reduce setup friction.

Also applies to: 418-424, 430-431

scripts/build.mts (1)

30-39: Consider watching/copying assets during dev.

Even with esbuild watch, asset edits (images/css/html) won’t be recopied. Option: integrate a small asset watcher calling copyAssets() and maybe touch a file to trigger web‑ext reload.

scripts/start.mts (2)

68-86: Fix Windows default Zotero path fallback.

Zotero 7 installs to “Program Files” (64‑bit). Prefer probing both locations.

Apply this diff:

-  if (process.platform === 'win32') {
-    return 'C:/Program Files (x86)/Zotero/Zotero.exe';
-  }
+  if (process.platform === 'win32') {
+    const candidates = [
+      'C:/Program Files/Zotero/Zotero.exe',
+      'C:/Program Files (x86)/Zotero/Zotero.exe',
+    ];
+    const found = candidates.find((p) => fs.existsSync(p));
+    assert.ok(found, 'Invalid path to Zotero');
+    return found!;
+  }

125-156: Ensure log directory exists and guard stream errors.

If logFile’s directory is missing, createWriteStream fails. Add ensureDirSync, and log stream errors.

Apply this diff:

   const { logFile, preserveLog } = config.zotero || {};
   if (!logFile) return;
+  fs.ensureDirSync(path.dirname(logFile));

@@
-  const logStream = fs.createWriteStream(logFile, { flags });
+  const logStream = fs.createWriteStream(logFile, { flags });
+  logStream.on('error', (err) => {
+    console.warn(`Log stream error: ${err?.message ?? err}`);
+  });
scripts/generate-update-manifest.mts (1)

59-60: Document the JSON-based update.rdf for legacy compatibility
The xpi.updateURL in package.json points to updates.json, so copying JSON into update.rdf is intentional. Add a brief comment in scripts/generate-update-manifest.mts explaining that update.rdf contains the same JSON manifest for legacy consumers.

types/web-ext.d.ts (1)

84-87: Consider using empty interfaces without eslint-disable comments.

The ESLint disable comments for empty object types might be unnecessary. Empty interfaces are valid and commonly used in TypeScript for extensibility points.

Apply this diff to remove the ESLint disable comments:

  // Docs command types
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
  export interface DocsOptions {
    // No specific options documented for docs command
  }

  // Dump config command types
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
  export interface DumpConfigOptions extends BaseOptions {
    // Inherits from BaseOptions
  }

Also applies to: 90-93

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 490bcbb and 8cc90d5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • .gitignore (1 hunks)
  • README.md (1 hunks)
  • package.json (1 hunks)
  • scripts/build.mts (1 hunks)
  • scripts/create-xpi.mts (1 hunks)
  • scripts/generate-fluent-types.mts (1 hunks)
  • scripts/generate-install-manifest.mts (0 hunks)
  • scripts/generate-update-manifest.mts (2 hunks)
  • scripts/start.mts (4 hunks)
  • scripts/utils/copy-assets.mts (1 hunks)
  • scripts/utils/generate-install-manifest.mts (1 hunks)
  • scripts/utils/paths.mts (1 hunks)
  • scripts/utils/version.mts (1 hunks)
  • types/web-ext.d.ts (1 hunks)
  • zotero.config.example.json (0 hunks)
💤 Files with no reviewable changes (2)
  • scripts/generate-install-manifest.mts
  • zotero.config.example.json
🧰 Additional context used
🧬 Code graph analysis (8)
scripts/build.mts (2)
scripts/utils/generate-install-manifest.mts (1)
  • generateInstallManifest (12-36)
scripts/utils/copy-assets.mts (1)
  • copyAssets (28-46)
scripts/create-xpi.mts (2)
scripts/utils/paths.mts (1)
  • buildDir (5-5)
scripts/utils/version.mts (1)
  • getVersion (13-27)
scripts/generate-fluent-types.mts (1)
scripts/copy-assets.mts (2)
  • filter (31-37)
  • removeEmptyDirectories (11-26)
scripts/utils/generate-install-manifest.mts (2)
scripts/utils/paths.mts (2)
  • buildDir (5-5)
  • relativeToRoot (10-10)
scripts/utils/version.mts (1)
  • getVersion (13-27)
scripts/utils/version.mts (2)
scripts/utils/paths.mts (2)
  • genDir (6-6)
  • relativeToRoot (10-10)
scripts/version.mts (4)
  • getVersion (15-26)
  • getPatchBumpVersion (36-38)
  • getLocalVersion (28-30)
  • getPrereleaseVersion (32-34)
scripts/utils/copy-assets.mts (1)
scripts/utils/paths.mts (3)
  • srcDir (7-7)
  • buildDir (5-5)
  • relativeToRoot (10-10)
scripts/start.mts (4)
scripts/utils/version.mts (1)
  • getVersion (13-27)
scripts/build.mts (1)
  • build (13-44)
types/web-ext.d.ts (2)
  • cmd (172-172)
  • MultiExtensionRunner (119-121)
scripts/utils/paths.mts (1)
  • buildDir (5-5)
scripts/generate-update-manifest.mts (1)
scripts/utils/version.mts (1)
  • getVersion (13-27)
🔇 Additional comments (13)
scripts/generate-fluent-types.mts (1)

6-6: Import path update: LGTM

scripts/utils/generate-install-manifest.mts (2)

5-5: JSON import uses with attribute; confirm Node baseline or switch to assert

with { type: 'json' } is newer; Node 20 LTS widely supports assert { type: 'json' }. Either set engines to Node ≥22 or use assert for broader support.

-import pkg from '../../package.json' with { type: 'json' };
+import pkg from '../../package.json' assert { type: 'json' };

22-23: Double‑check manifest_version for Zotero 7/8

Zotero typically uses MV2 today; please confirm MV2 is still required/valid for your target (7.0/8.0.*).

scripts/create-xpi.mts (2)

14-15: Top‑level await for version: LGTM


7-7: JSON import attribute: align with Node baseline

Same concern as elsewhere—prefer assert unless Node ≥22 is enforced.

-import pkg from '../package.json' with { type: 'json' };
+import pkg from '../package.json' assert { type: 'json' };
scripts/utils/version.mts (1)

13-27: Overall versioning flow: LGTM

Lazy read/write of gen/version.json and logging are clear and predictable.

scripts/build.mts (2)

17-24: Consider bundling bootstrap or documenting reasons not to.

src/bootstrap.ts is built without bundle: true. If it imports local modules, missing bundling could break in Zotero’s environment. If intentional, add a comment; otherwise set bundle: true.


46-52: Use a Node‑portable main‑module check or require Node ≥24.

import.meta.main was merged into Node.js (PR #57804, merged May 26, 2025); a TypeScript .ts/.mts edge case was later fixed (PR #58661). (github.com)

If you need to support Node <24 or older tooling, replace the check with a portable guard, e.g.:
import { pathToFileURL } from 'node:url';
const isMain = import.meta.url === pathToFileURL(process.argv[1] ?? '').href;
if (isMain) { /* ... */ }. (2ality.com)

Likely an incorrect or invalid review comment.

scripts/start.mts (2)

90-99: Good: build before start with sourcemaps.

Nice flow; ensures manifest/assets exist and enables reload after rebuild.


105-115: Cleanup hook correctly disposes build context.

Solid use of registerCleanup to stop the watcher.

Also applies to: 117-121

scripts/generate-update-manifest.mts (1)

45-53: Validate updates.json schema for Zotero fields — confirm version strings

applications.zotero is correct for Zotero 7 (gecko is only needed for Zotero 6). Confirm pkg.xpi.zoteroMinVersion and pkg.xpi.zoteroMaxVersion emit Zotero‑compatible strings (e.g., strict_min_version like "6.999" to allow Zotero 7 betas; strict_max_version like "7.0.*" to limit to a tested minor series).
Location: scripts/generate-update-manifest.mts lines 45–53.

types/web-ext.d.ts (2)

1-173: LGTM! Comprehensive and well-structured TypeScript declarations.

The type definitions provide excellent coverage of the web-ext API with proper interface hierarchies, clear documentation, and thoughtful organization. The structure effectively captures the command-based architecture and extension runner lifecycle.


167-168: Keep the eslint-disable — rule enforced globally

eslint.config.mjs sets "import/no-default-export" to "error"; matching inline disables exist (types/components/virtualized-table.d.ts, vitest.config.mts). The disable in types/web-ext.d.ts is consistent and should remain.

Comment thread scripts/build.mts Outdated
Comment on lines +8 to +9
import { genDir, relativeToRoot } from './utils/paths.mts';
import { getVersion } from './utils/version.mts';
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Confirm Node runtime supports import attributes and TLA.

This relies on JSON import attributes (with) and top‑level await. Ensure the project enforces a Node version that supports both (e.g., engines field and/or .nvmrc).

Also applies to: 18-18


🏁 Script executed:

#!/bin/bash
# Verify Node engine and .nvmrc
jq -r '.engines.node // "missing engines.node"' package.json || true
[ -f .nvmrc ] && cat .nvmrc || echo "missing .nvmrc"
# Grep for ' with { type: \"json\" }' usage
rg -nP "with\s*{\s*type:\s*'json'\s*}" -g '!node_modules/**'

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== package.json (full) ==="
if [ -f package.json ]; then
  jq -r '.' package.json || cat package.json
else
  echo "no package.json"
fi

echo
echo "=== package.json: type, engines.node, packageManager, volta ==="
if [ -f package.json ]; then
  jq -r '.type // "(no type)"' package.json || true
  jq -r '.engines.node // "(missing engines.node)"' package.json || true
  jq -r '.packageManager // "(no packageManager)"' package.json || true
  jq -r '.volta.node // "(no volta.node)"' package.json || true
fi

echo
echo "=== .nvmrc ==="
if [ -f .nvmrc ]; then cat .nvmrc; else echo "missing .nvmrc"; fi

echo
echo "=== .tool-versions ==="
[ -f .tool-versions ] && cat .tool-versions || echo "missing .tool-versions"

echo
echo "=== scripts/generate-update-manifest.mts (first 200 lines) ==="
if [ -f scripts/generate-update-manifest.mts ]; then sed -n '1,200p' scripts/generate-update-manifest.mts; else echo "missing scripts/generate-update-manifest.mts"; fi

echo
echo "=== Searching for import assertions and JSON imports ==="
rg -n --hidden -uu -S "assert\\s*\\{\\s*type\\s*:\\s*['\\\"]json['\\\"]\\s*\\}" --glob '!node_modules/**' || true
rg -n --hidden -uu -S "with\\s*\\{\\s*type\\s*:\\s*['\\\"]json['\\\"]\\s*\\}" --glob '!node_modules/**' || true
rg -n --hidden -uu -S "import\\s+[^;]+\\.json" --glob '!node_modules/**' || true

echo
echo "=== Searching for await in .mts/.mjs/.ts ==="
rg -n --hidden -uu -S --glob '!node_modules/**' --glob '**/*.mts' '\\bawait\\b' -C3 || true
rg -n --hidden -uu -S --glob '!node_modules/**' --glob '**/*.mjs' '\\bawait\\b' -C3 || true
rg -n --hidden -uu -S --glob '!node_modules/**' --glob '**/*.ts' '\\bawait\\b' -C3 || true

echo
echo "=== Searching for Node version config in CI/workflows ==="
ls -la .github/workflows || true
rg -n --hidden -uu -S "actions/setup-node|node-version" .github || true

echo
echo "=== Searching for engines in other package.json files ==="
rg -n --hidden -uu -S '"engines"' --glob '!node_modules/**' || true

echo
echo "=== Searching for tsx/ts-node/typescript in deps ==="
if [ -f package.json ]; then
  jq -r '((.devDependencies // {}) + (.dependencies // {})) | keys[]' package.json | rg -n 'tsx|ts-node|typescript|esbuild|tsup|esbuild-runner|tsx|@swc' || true
fi

echo
echo "=== Done ==="

Length of output: 6685


Require Node >= 18.20.0 (import attributes 'with')

scripts/generate-update-manifest.mts uses import ... with { type: 'json' } and top-level await; import attributes (with) are available from Node 18.20.0, and top-level await was unflagged in Node 14.8.0. (nodejs.org)

  • Add an engines.node entry (e.g. "node": ">=18.20.0") to package.json and/or commit a .nvmrc or .node-version set to 18.20.0.
  • CI references '.node-version' in .github/workflows/* (release.yml, build.yml) — ensure that file exists or pin node-version directly in those workflows.
  • Location: scripts/generate-update-manifest.mts (uses JSON import attribute + TLA).
🤖 Prompt for AI Agents
In scripts/generate-update-manifest.mts around lines 8-9 it uses import
attributes (with { type: 'json' }) which requires Node >= 18.20.0; add a
engines.node entry in package.json (e.g. "node": ">=18.20.0") and commit a
runtime pin file (.nvmrc or .node-version) containing 18.20.0, and/or update
.github/workflows/* to pin node-version directly to 18.20.0 so CI and local
developers run the required Node version; ensure the chosen approach is
consistent (package.json engines + .nvmrc/.node-version and CI references that
file or CI uses the same pinned version).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm already using a .node-version file.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for .node-version file and its content
echo "=== Checking for .node-version file ==="
if [ -f .node-version ]; then
  echo "✓ .node-version exists"
  echo "Content:"
  cat .node-version
  echo ""
else
  echo "✗ .node-version not found"
fi

# Also check for .nvmrc as alternative
echo "=== Checking for .nvmrc file ==="
if [ -f .nvmrc ]; then
  echo "✓ .nvmrc exists" 
  echo "Content:"
  cat .nvmrc
  echo ""
else
  echo "✗ .nvmrc not found"
fi

# Search for any Node version files
echo "=== Searching for Node version files ==="
find . -maxdepth 2 -name ".node-version" -o -name ".nvmrc" -o -name ".tool-versions" | head -10

# Check if the files are in the current working directory
echo "=== Current directory listing (showing hidden files) ==="
ls -la | grep -E "(node|nvmrc)"

Length of output: 311


@dvanoni You're absolutely right! I can confirm that .node-version exists and specifies Node 24.3.0, which fully supports both import attributes (with { type: 'json' }) and top-level await used in the code. My apologies for the confusion - your setup is perfectly adequate for the features being used in scripts/generate-update-manifest.mts.

The Node version requirement is well satisfied, so there's no action needed here.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Comment thread scripts/utils/paths.mts
import path from 'node:path';

export const rootDir = path.join(import.meta.dirname, '..');
export const rootDir = path.join(import.meta.dirname, '../..');
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid Node 22‑only import.meta.dirname; use fileURLToPath for broader Node support

import.meta.dirname requires Node ≥22. Use fileURLToPath to work on Node 18/20/22.

+import { fileURLToPath } from 'node:url';
 export const rootDir = path.join(import.meta.dirname, '../..');
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+export const rootDir = path.join(__dirname, '../..');

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In scripts/utils/paths.mts around line 3, the code uses import.meta.dirname
which only works on Node 22+; replace it with the Node-compatible pattern using
fileURLToPath(import.meta.url) to compute __dirname and then join with '../..'.
Import fileURLToPath from 'url' (and keep path import) and use
path.join(fileURLToPath(import.meta.url), '..', '..') or compute a dirname
variable via path.dirname(fileURLToPath(import.meta.url)) before joining.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're using other new features of Node, we require version 24.3.0 or higher.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dvanoni Thank you for the clarification! Since the project requires Node 24.3.0 or higher, using import.meta.dirname is perfectly appropriate and my compatibility suggestion is unnecessary.


✏️ Learnings added
Learnt from: dvanoni
PR: dvanoni/notero#738
File: scripts/utils/paths.mts:3-3
Timestamp: 2025-09-22T14:47:36.288Z
Learning: The notero project requires Node.js version 24.3.0 or higher, so modern Node.js features like import.meta.dirname can be used without compatibility concerns.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread scripts/utils/version.mts
Comment thread scripts/utils/version.mts Outdated
Comment on lines +15 to +19
const versionModule = (await import(versionJsonPath, {
with: { type: 'json' },
})) as { default: string };
const version = versionModule.default;
console.log(`Found ${relativeToRoot(versionJsonPath)} with ${version}`);
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Dynamic JSON import: use file URL and assert for cross‑platform safety

Absolute Windows paths can break import(). Convert to file:// and use assert.

-    const versionModule = (await import(versionJsonPath, {
-      with: { type: 'json' },
-    })) as { default: string };
+    import { pathToFileURL } from 'node:url';
+    const url = pathToFileURL(versionJsonPath).href;
+    const versionModule = (await import(url, {
+      assert: { type: 'json' },
+    })) as { default: string };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const versionModule = (await import(versionJsonPath, {
with: { type: 'json' },
})) as { default: string };
const version = versionModule.default;
console.log(`Found ${relativeToRoot(versionJsonPath)} with ${version}`);
import { pathToFileURL } from 'node:url';
const url = pathToFileURL(versionJsonPath).href;
const versionModule = (await import(url, {
assert: { type: 'json' },
})) as { default: string };
const version = versionModule.default;
console.log(`Found ${relativeToRoot(versionJsonPath)} with ${version}`);
🤖 Prompt for AI Agents
In scripts/utils/version.mts around lines 15 to 19, the dynamic JSON import uses
a plain filesystem path which can break on Windows; convert the path to a
file:// URL and use the import assertion form. Create a file URL from
versionJsonPath (e.g. via pathToFileURL(versionJsonPath).href), then call
import(fileUrl, { assert: { type: 'json' } }) and read the default export; keep
the existing cast to { default: string } and update the log call to use the
imported value.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to convert to file URLs when using version 24.3.0 of Node?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dvanoni Yes, you still need to convert to file URLs when using Node.js 24.3.0.

The current Node.js v24.8.0 documentation explicitly states: "Given the differences between URL and path resolution (such as percent encoding details), it is recommended to use url.pathToFileURL when importing a path."

The Windows compatibility issues that were reported in GitHub issue #31710 from 2020 (showing "ERR_UNSUPPORTED_ESM_URL_SCHEME" errors) haven't been fundamentally resolved because they stem from how Windows absolute paths are interpreted as URLs. The issue affects both static and dynamic ES imports.

Even recent examples from Stack Overflow show developers still using pathToFileURL to convert absolute paths like "C:\Users\sparky\example\plugins\plugin1.js" to proper file URLs like "file:///C:/Users/sparky/example/plugins/plugin1.js" for cross-platform compatibility.

So the original suggestion remains valid - using pathToFileURL ensures cross-platform compatibility:

import { pathToFileURL } from 'node:url';
const url = pathToFileURL(versionJsonPath).href;
const versionModule = (await import(url, {
  with: { type: 'json' },
})) as { default: string };

This approach works reliably across all platforms, including Windows, in Node.js 24.3.0 and later versions.

🐰💭

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Thanks! Is this only necessary with the await import syntax, or do I need this in all places where I use with { type: 'json' }?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@dvanoni The file URL conversion is only necessary for dynamic imports (await import() syntax) that use absolute filesystem paths.

Here are the different scenarios:

✅ No conversion needed:

  • Static imports with relative paths (like in your code):

    import pkg from '../../package.json' with { type: 'json' };

    These work fine across all platforms because the module resolver handles the path resolution at compile time.

  • Dynamic imports with relative paths:

    await import('./config.json', { with: { type: 'json' } })

    Also work fine because relative paths don't trigger Windows path interpretation issues.

⚠️ Conversion needed:

  • Dynamic imports with absolute filesystem paths (like in your getVersion() function):
    // This can break on Windows
    await import('/absolute/path/to/file.json', { with: { type: 'json' } })
    
    // Convert to file URL first
    import { pathToFileURL } from 'node:url';
    const url = pathToFileURL(absolutePath).href;
    await import(url, { with: { type: 'json' } })

The issue isn't with the with { type: 'json' } syntax itself - it's specifically about how Windows interprets absolute filesystem paths in dynamic imports. Static imports and relative paths in dynamic imports work fine without conversion.


🏁 Script executed:

#!/bin/bash
# Search for all occurrences of 'with { type: 'json' }' in the codebase
rg -n "with \{ type: 'json' \}" --type=ts --type=js -C2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for 'with' and 'type' and 'json' patterns more broadly
rg -n "with.*type.*json" --type=ts --type=js -C2

# Also search for import assertions with different spacing
rg -n "with.*{.*type.*:.*['\"]json['\"].*}" --type=ts --type=js -C2

# Search for any import statements with json
rg -n "import.*\.json" --type=ts --type=js -C2

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Search for import statements with json files
rg -n "import.*\.json" -C2

# Search for with type json with proper escaping
rg -n 'with.*type.*json' -C2

# Search specifically for the pattern we saw in the file
rg -n 'type.*json' -C2

# Look for all import patterns in the scripts directory
find scripts -name "*.mts" -exec grep -n "import" {} \; -print

Length of output: 9744


@dvanoni Perfect! Now I can give you a complete answer based on your actual codebase.

Looking at your code, you have both patterns:

✅ These static imports are fine as-is (no conversion needed):

// In create-xpi.mts, generate-update-manifest.mts:
import pkg from '../package.json' with { type: 'json' };

// In generate-install-manifest.mts, version.mts:
import pkg from '../../package.json' with { type: 'json' };

⚠️ This dynamic import needs the file URL conversion:

// In version.mts - this uses an absolute path (versionJsonPath)
const versionModule = (await import(versionJsonPath, {
  with: { type: 'json' },
})) as { default: string };

The key difference is that versionJsonPath is constructed as path.join(genDir, 'version.json'), which creates an absolute filesystem path. This is the one that can break on Windows.

So to answer your question directly: You only need the file URL conversion for the dynamic import in getVersion(). All your static imports with relative paths work fine across all platforms without any changes.

The with { type: 'json' } syntax itself isn't the problem - it's specifically the combination of dynamic import + absolute filesystem paths that needs the conversion.

Comment thread types/web-ext.d.ts
}

export interface RunDependencies {
getValidatedManifest?: (sourceDir: string) => Promise<unknown>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify web-ext's getValidatedManifest function signature.

The getValidatedManifest function signature should be confirmed against the actual web-ext implementation to ensure accuracy.


🌐 Web query:

web-ext getValidatedManifest function signature TypeScript

💡 Result:

There are no official TypeScript types in web-ext, but in the web-ext API the getValidatedManifest option is a function that should return the (validated) manifest object — synchronous or Promise. A practical TS signature you can use:

type GetValidatedManifest = () => any | Promise;

and in options:

getValidatedManifest?: GetValidatedManifest;

References: example usage in web-ext docs/npm pages. [1][2]

Sources

  • web-ext npm package docs (example using getValidatedManifest). [1]
  • web-ext README / skypack example (shows getValidatedManifest: () => ({ name: ..., version: ... })). [2]

Update getValidatedManifest signature to match web-ext API

web-ext's getValidatedManifest is a zero-argument function that may return the manifest synchronously or a Promise — update types/web-ext.d.ts (line 55) to:
getValidatedManifest?: () => unknown | Promise;

🤖 Prompt for AI Agents
In types/web-ext.d.ts around line 55, the getValidatedManifest signature
currently expects a string argument and a Promise return; change it to a
zero-argument function returning either a synchronous value or a Promise by
replacing the line with: getValidatedManifest?: () => unknown |
Promise<unknown>; this aligns the type with the web-ext API.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (13)
package.json (2)

76-76: Add watcher stability to avoid copying partially written files.

Since chokidar v4 is introduced, consider enabling awaitWriteFinish in your watcher to prevent copying half-written assets during saves.

You can apply this in scripts/utils/copy-assets.mts (Line 21):

-  const watcher = watch(srcDir, { ignored: IGNORE_PATTERNS, persistent });
+  const watcher = watch(srcDir, {
+    ignored: IGNORE_PATTERNS,
+    persistent,
+    awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
+  });

96-97: Pin web-ext to a stable minor and add a lint script.

web-ext CLIs occasionally ship breaking changes; consider pinning to a minor (e.g., ~8.10.0) and adding a script to catch manifest issues in CI.

-    "web-ext": "^8.10.0"
+    "web-ext": "~8.10.0"

Add in scripts:

+    "xpi:lint": "web-ext lint --source-dir build"

Optionally add engines to enforce Node ≥24.3.0:

   "homepage": "https://github.com/dvanoni/notero",
+  "engines": { "node": ">=24.3.0" },

Would you like a small CI step added to run xpi:lint during verify?

scripts/utils/version.mts (1)

25-29: Avoid sync file I/O in async code path.

Replace outputJsonSync with async outputJson to keep the event loop responsive during builds.

-  fs.outputJsonSync(versionJsonPath, version);
+  await fs.outputJson(versionJsonPath, version);
scripts/utils/copy-assets.mts (3)

21-21: Add awaitWriteFinish to prevent partial copies.

Helps on editors that save via temp files/chunked writes.

-  const watcher = watch(srcDir, { ignored: IGNORE_PATTERNS, persistent });
+  const watcher = watch(srcDir, {
+    ignored: IGNORE_PATTERNS,
+    persistent,
+    awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
+  });

24-31: Prefer async fs operations in hot path.

Synchronous copy blocks the event loop during frequent saves.

-    .on('add', (srcPath) => {
+    .on('add', async (srcPath) => {
       const destPath = srcPathToBuildPath(srcPath);
       console.log(`Copying asset: ${relativeToRoot(srcPath)}`);
-      fs.copySync(srcPath, destPath);
+      await fs.copy(srcPath, destPath);
     })

55-64: Use async fs for change/unlink; handle missing targets gracefully.

Reduces blocking and makes removals tolerant to races.

-    .on('change', (srcPath) => {
+    .on('change', async (srcPath) => {
       const destPath = srcPathToBuildPath(srcPath);
       console.log(`Copying updated asset: ${relativeToRoot(srcPath)}`);
-      fs.copySync(srcPath, destPath);
+      await fs.copy(srcPath, destPath);
     })
-    .on('unlink', (srcPath) => {
+    .on('unlink', async (srcPath) => {
       const destPath = srcPathToBuildPath(srcPath);
       console.log(`Removing deleted asset: ${relativeToRoot(srcPath)}`);
-      fs.removeSync(destPath);
+      await fs.remove(destPath);
     });
scripts/start.mts (4)

44-46: Consider manifest validation or surfacing lint errors early.

Reading JSON is fine; optionally run web‑ext lint first and reuse its parsed manifest to fail fast on schema errors.

Would you like me to wire xpi:lint here and show errors before launching Zotero?


105-115: web-ext run options: register cleanup via async to ensure orderly shutdown.

If registerCleanup awaits promises, drop void to avoid racing shutdown.

-  if (cleanup) {
-    extensionRunner.registerCleanup(() => {
-      void cleanup();
-    });
-  }
+  if (cleanup) {
+    extensionRunner.registerCleanup(async () => {
+      await cleanup();
+    });
+  }

126-157: Harden logging: ensure directory exists and add newlines.

Prevents EINVAL on missing dirs and improves readability.

-  const flags = preserveLog ? 'a' : 'w';
-  const logStream = fs.createWriteStream(logFile, { flags });
+  const flags = preserveLog ? 'a' : 'w';
+  await fs.ensureDir(path.dirname(logFile));
+  const logStream = fs.createWriteStream(logFile, { flags });
@@
-  const writeToLog = (data: unknown) => {
+  const writeToLog = (data: unknown) => {
     const timestamp = new Date().toISOString();
-    logStream.write(`[${timestamp}] ${String(data)}`);
+    logStream.write(`[${timestamp}] ${String(data)}\n`);
   };

67-86: Prefer 64-bit Program Files path on Windows
Use C:/Program Files/Zotero/Zotero.exe with a fallback to C:/Program Files (x86)/Zotero/Zotero.exe:

-  if (process.platform === 'win32') {
-    return 'C:/Program Files (x86)/Zotero/Zotero.exe';
-  }
+  if (process.platform === 'win32') {
+    const x64 = 'C:/Program Files/Zotero/Zotero.exe';
+    const x86 = 'C:/Program Files (x86)/Zotero/Zotero.exe';
+    return fs.existsSync(x64) ? x64 : x86;
+  }
scripts/build.mts (3)

13-15: Single source of truth for build output dir.

Avoid hardcoding 'build' here; reuse buildDir from utils/paths.mts to prevent drift with other scripts.

-import esbuild, { type Plugin } from 'esbuild';
+import esbuild, { type Plugin } from 'esbuild';
+import { buildDir } from './utils/paths.mts';
@@
-const OUTDIR = 'build';
-const TARGET = 'firefox115';
+const TARGET = 'firefox115';
@@
-    outdir: OUTDIR,
+    outdir: buildDir,
@@
-    outdir: OUTDIR,
+    outdir: buildDir,

Also applies to: 33-39, 47-51


66-74: Ensure cleanup runs fully even if one disposer throws.

Try/finally each step so one failure doesn’t skip the rest.

-  const cleanup: CleanupFunction = async () => {
-    console.log('Stopping build watcher');
-    await bootstrapContext.dispose();
-    await contentContext.dispose();
-    await cleanupAssetWatcher();
-  };
+  const cleanup: CleanupFunction = async () => {
+    console.log('Stopping build watcher');
+    const tasks = [
+      bootstrapContext.dispose().catch((e) => console.warn('bootstrap dispose failed:', e)),
+      contentContext.dispose().catch((e) => console.warn('content dispose failed:', e)),
+      cleanupAssetWatcher().catch((e) => console.warn('asset watcher cleanup failed:', e)),
+    ];
+    await Promise.all(tasks);
+  };

76-91: Add SIGTERM handling for graceful shutdown
Extend the cleanup promise in scripts/build.mts to listen for SIGTERM alongside SIGINT.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc90d5 and 5efa0d3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • .gitignore (1 hunks)
  • package.json (3 hunks)
  • scripts/build.mts (1 hunks)
  • scripts/start.mts (4 hunks)
  • scripts/utils/copy-assets.mts (1 hunks)
  • scripts/utils/version.mts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .gitignore
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-22T14:47:36.288Z
Learnt from: dvanoni
PR: dvanoni/notero#738
File: scripts/utils/paths.mts:3-3
Timestamp: 2025-09-22T14:47:36.288Z
Learning: The notero project requires Node.js version 24.3.0 or higher, so modern Node.js features like import.meta.dirname can be used without compatibility concerns.

Applied to files:

  • scripts/utils/version.mts
🧬 Code graph analysis (4)
scripts/start.mts (4)
scripts/utils/version.mts (1)
  • getVersion (14-29)
scripts/build.mts (1)
  • build (27-74)
types/web-ext.d.ts (2)
  • cmd (172-172)
  • MultiExtensionRunner (119-121)
scripts/utils/paths.mts (1)
  • buildDir (5-5)
scripts/build.mts (2)
scripts/utils/generate-install-manifest.mts (1)
  • generateInstallManifest (12-36)
scripts/utils/copy-assets.mts (2)
  • copyAssets (45-49)
  • copyAndWatchAssets (51-76)
scripts/utils/version.mts (1)
scripts/utils/paths.mts (2)
  • genDir (6-6)
  • relativeToRoot (10-10)
scripts/utils/copy-assets.mts (1)
scripts/utils/paths.mts (3)
  • buildDir (5-5)
  • srcDir (7-7)
  • relativeToRoot (10-10)
🔇 Additional comments (3)
package.json (1)

7-7: Script addition looks good; aligns with watchable build flow.

No issues. This pairs correctly with scripts/build.mts watch mode.

scripts/utils/version.mts (2)

14-21: Dynamic JSON import via file URL: LGTM.

Correct use of pathToFileURL + JSON modules for cross‑platform dynamic import. Nice logging.


48-54: Nullish-coalesce GITHUB_RUN_NUMBER in getPrereleaseVersion
Use

const run = process.env.GITHUB_RUN_NUMBER ?? 'local';
return `${baseVersion}-${run}`;

to prevent 1.2.3-undefined suffixes. GitHub Actions always provides GITHUB_RUN_NUMBER (so CI tags stay numeric), and local builds use getLocalVersion, so the 'local' fallback won’t conflict.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5efa0d3 and 6b8b2e9.

📒 Files selected for processing (3)
  • package.json (4 hunks)
  • scripts/build.mts (1 hunks)
  • scripts/utils/version.mts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-22T14:47:36.288Z
Learnt from: dvanoni
PR: dvanoni/notero#738
File: scripts/utils/paths.mts:3-3
Timestamp: 2025-09-22T14:47:36.288Z
Learning: The notero project requires Node.js version 24.3.0 or higher, so modern Node.js features like import.meta.dirname can be used without compatibility concerns.

Applied to files:

  • scripts/utils/version.mts
  • package.json
🧬 Code graph analysis (2)
scripts/utils/version.mts (1)
scripts/utils/paths.mts (2)
  • genDir (6-6)
  • relativeToRoot (10-10)
scripts/build.mts (3)
scripts/utils/generate-install-manifest.mts (1)
  • generateInstallManifest (12-36)
scripts/utils/paths.mts (1)
  • buildDir (5-5)
scripts/utils/copy-assets.mts (2)
  • copyAssets (45-49)
  • copyAndWatchAssets (51-76)

Comment thread scripts/utils/version.mts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b8b2e9 and b28d778.

📒 Files selected for processing (1)
  • scripts/utils/version.mts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-22T14:47:36.288Z
Learnt from: dvanoni
PR: dvanoni/notero#738
File: scripts/utils/paths.mts:3-3
Timestamp: 2025-09-22T14:47:36.288Z
Learning: The notero project requires Node.js version 24.3.0 or higher, so modern Node.js features like import.meta.dirname can be used without compatibility concerns.

Applied to files:

  • scripts/utils/version.mts
🧬 Code graph analysis (1)
scripts/utils/version.mts (1)
scripts/utils/paths.mts (2)
  • genDir (6-6)
  • relativeToRoot (10-10)

Comment thread scripts/utils/version.mts
@dvanoni dvanoni changed the title build: Enable live reloading using web-ext and esbuild build: Enable live reloading using web-ext, esbuild, and chokidar Sep 28, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b28d778 and e34a2ea.

📒 Files selected for processing (1)
  • scripts/utils/version.mts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-22T14:47:36.288Z
Learnt from: dvanoni
PR: dvanoni/notero#738
File: scripts/utils/paths.mts:3-3
Timestamp: 2025-09-22T14:47:36.288Z
Learning: The notero project requires Node.js version 24.3.0 or higher, so modern Node.js features like import.meta.dirname can be used without compatibility concerns.

Applied to files:

  • scripts/utils/version.mts
🧬 Code graph analysis (1)
scripts/utils/version.mts (2)
scripts/utils/paths.mts (2)
  • genDir (6-6)
  • relativeToRoot (10-10)
scripts/version.mts (4)
  • getVersion (15-26)
  • getPatchBumpVersion (36-38)
  • getLocalVersion (28-30)
  • getPrereleaseVersion (32-34)

Comment thread scripts/utils/version.mts
Comment on lines +13 to +25
export async function getVersion(): Promise<string> {
const versionJsonExists = await fs.pathExists(versionJsonPath);

function getVersion(): string {
if (versionJsonExists) {
const version = (await fs.readJson(versionJsonPath)) as string;
console.log(`Found ${relativeToRoot(versionJsonPath)} with ${version}`);
return version;
}

const version = computeVersion();
console.log(`Writing ${relativeToRoot(versionJsonPath)} with ${version}`);
await fs.outputJson(versionJsonPath, version);
return version;
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Version cache never refreshes after package.json bump

Line 17 returns the cached gen/version.json unconditionally. After the first run, getVersion() stops recomputing, so if we bump package.json (or the GH env changes), subsequent builds keep serving the stale version string unless the developer manually deletes gen/version.json. That breaks the whole “live reload” promise because the new bundle keeps the old version. Please recompute when the derived version diverges from the cached value and overwrite the file accordingly.

-  if (versionJsonExists) {
-    const version = (await fs.readJson(versionJsonPath)) as string;
-    console.log(`Found ${relativeToRoot(versionJsonPath)} with ${version}`);
-    return version;
-  }
-
-  const version = computeVersion();
-  console.log(`Writing ${relativeToRoot(versionJsonPath)} with ${version}`);
-  await fs.outputJson(versionJsonPath, version);
-  return version;
+  if (versionJsonExists) {
+    const cachedVersion = (await fs.readJson(versionJsonPath)) as string;
+    const computedVersion = computeVersion();
+
+    if (cachedVersion === computedVersion) {
+      console.log(`Found ${relativeToRoot(versionJsonPath)} with ${cachedVersion}`);
+      return cachedVersion;
+    }
+
+    console.log(
+      `Updating ${relativeToRoot(versionJsonPath)} from ${cachedVersion} to ${computedVersion}`,
+    );
+    await fs.outputJson(versionJsonPath, computedVersion);
+    return computedVersion;
+  }
+
+  const version = computeVersion();
+  console.log(`Writing ${relativeToRoot(versionJsonPath)} with ${version}`);
+  await fs.outputJson(versionJsonPath, version);
+  return version;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getVersion(): Promise<string> {
const versionJsonExists = await fs.pathExists(versionJsonPath);
function getVersion(): string {
if (versionJsonExists) {
const version = (await fs.readJson(versionJsonPath)) as string;
console.log(`Found ${relativeToRoot(versionJsonPath)} with ${version}`);
return version;
}
const version = computeVersion();
console.log(`Writing ${relativeToRoot(versionJsonPath)} with ${version}`);
await fs.outputJson(versionJsonPath, version);
return version;
export async function getVersion(): Promise<string> {
const versionJsonExists = await fs.pathExists(versionJsonPath);
if (versionJsonExists) {
const cachedVersion = (await fs.readJson(versionJsonPath)) as string;
const computedVersion = computeVersion();
if (cachedVersion === computedVersion) {
console.log(`Found ${relativeToRoot(versionJsonPath)} with ${cachedVersion}`);
return cachedVersion;
}
console.log(
`Updating ${relativeToRoot(versionJsonPath)} from ${cachedVersion} to ${computedVersion}`,
);
await fs.outputJson(versionJsonPath, computedVersion);
return computedVersion;
}
const version = computeVersion();
console.log(`Writing ${relativeToRoot(versionJsonPath)} with ${version}`);
await fs.outputJson(versionJsonPath, version);
return version;
}
🤖 Prompt for AI Agents
In scripts/utils/version.mts around lines 13 to 25, the function currently
returns the cached gen/version.json unconditionally which prevents updates after
package.json or env changes; change the logic to read the cached version,
compute the current derived version, compare them, and if they differ overwrite
gen/version.json (with a log) and return the newly computed version; keep the
fast-path when they match (log and return cached), and ensure you await
computeVersion() if it is async and use fs.outputJson to persist the new value.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't update the version during development, so we don't need to worry about live reloading it. I don't think this concern is applicable.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dvanoni, thank you for the clarification! You're absolutely right - if the version doesn't change during development, then the caching behavior works perfectly for your workflow. I understand that this concern isn't applicable to your development practices.

ᵔᴥᵔ


✏️ Learnings added
Learnt from: dvanoni
PR: dvanoni/notero#738
File: scripts/utils/version.mts:13-25
Timestamp: 2025-09-28T00:43:33.442Z
Learning: In the notero project, versions are not updated during development, so version caching in getVersion() doesn't need to handle live updates of package.json version changes during development workflow.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@dvanoni dvanoni added the maintenance Maintenance work that doesn't change functionality label Sep 28, 2025
@dvanoni dvanoni merged commit 7d55c5b into main Sep 28, 2025
5 of 6 checks passed
@dvanoni dvanoni deleted the live-reload branch September 28, 2025 00:44
dataymeric pushed a commit to dataymeric/anytero that referenced this pull request Feb 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance Maintenance work that doesn't change functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant