WebSystemInjection

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

WSI (Web System Injection) is a Chrome extension (Manifest V3) that injects per-domain custom features (“plugins”) into existing websites. Developers author plugins as ZIP bundles and import them through the popup UI; the extension then runs them locally in the developer’s own browser.

Full product spec is in doc/要件定義.md. The extension source lives in src/ and is loaded unpacked from that directory — there is no build step.

Commands

Install Playwright’s Chromium once per machine:

npm run playwright:install

E2E tests (they launch a real Chromium with the extension loaded from src/):

npm run test:e2e                     # headless-ish run (config forces headless: false)
npm run test:e2e:headed              # with visible browser
npm run test:e2e:debug               # Playwright inspector
npm run test:e2e:ui                  # Playwright UI mode
npx playwright test tests/e2e/popup.spec.js          # single file
npx playwright test -g "ZIPをインポート"             # single test by title

There is no lint or build command. Loading the extension manually: chrome://extensions → Developer Mode → “Load unpacked” → select src/.

Architecture

Three runtime layers communicate via chrome.runtime.sendMessage and window.postMessage:

  1. Service workersrc/background.js. Watches tabs.onUpdated, reads plugins + wsiEnabled from chrome.storage.local, filters by domain match, then uses chrome.userScripts.execute to load src/sdk/wsi-sdk.js and invoke the imported plugin in the page’s MAIN world. CSS is added by that user script, and a plugin that already ran in the current document is skipped (reason: 'already-ran'). Do not replace this with eval, new Function, or chrome.scripting; user-provided code must run through the User Scripts API for Manifest V3 policy compliance.
  2. Content scriptsrc/content-loader.js. Does not inject plugin code. It only bridges window.postMessagechrome.storage.local / chrome.runtime.sendMessage so that main-world plugin code can reach extension APIs (storage, fetch).
  3. Popup UIsrc/popup/. Handles ZIP import (via bundled src/lib/jszip.js), plugin list, per-plugin enable/disable, and the global on/off toggle. It warns when the browser’s Allow User Scripts toggle is disabled. All state is persisted in chrome.storage.local.

Storage shape (chrome.storage.local)

Plugin bundle format

A plugin ZIP contains plugin.json + main.js (+ optional CSS listed in styles[]). Validation happens in src/popup/popup.js validatePluginJsonid must match /^[a-zA-Z0-9-]+$/, domains[] must be non-empty. domains supports *.example.com wildcard subdomain matching, and "*" alone as a match-all pattern (see matchesDomain in src/background.js).

SDK surface (main-world only)

Exposed to plugin code via the WSI argument. The SDK is generated code. Its source of truth is packages/wsi_sdk in the WSIBrowser repository (shared with the Flutter app WSI Browser). Update packages/wsi_sdk/src/core there, run npm run build:sdk && npm run sync:wsi -w packages/wsi_sdk (copies dist/wsi-sdk-chrome.js here), preserve the callable __wsiRun(spec, pluginEntry) interface without dynamic code evaluation, and bump src/manifest.json. The Chrome-specific part is packages/wsi_sdk/src/adapters/chrome.js, which keeps the window.postMessage protocol below so src/content-loader.js is unchanged.

Conventions

Versioning (from .cursor/rules/plugin-versioning.mdc)

When re-creating a plugin ZIP, always bump plugin.json’s version:

Also bump the extension’s src/manifest.json version whenever you re-bundle WSI itself.

i18n

UI strings go through chrome.i18n.getMessage with message catalogs under src/_locales/ (ja, en, ko, zh_CN; default is ja). HTML uses data-i18n, data-i18n-title, data-i18n-html attributes — see initI18n in src/popup/popup.js. When adding a new UI string, add it to all four locales.

Sample plugins

samples/ holds reference plugins. Note that samples/dmm.co.jp/ and samples/mgstage.com/ are gitignored — they are local-only examples; don’t expect them in a fresh clone. samples/example.com/hello-world/ is the canonical committed sample.

Things to watch out for