Greasy Sprocket extension skeleton template
  • Shell 33.6%
  • TypeScript 31.4%
  • CSS 26.3%
  • HTML 7.1%
  • JavaScript 1.6%
Find a file
monyi 2ec4a78450
All checks were successful
ci / validate (push) Successful in 1m34s
ci / release (push) Successful in 1m23s
ci: guard de tag en cada paso del release (un push a main ya no pone el job en rojo)
2026-11-04 14:14:44 +01:00
.gitea/workflows ci: guard de tag en cada paso del release (un push a main ya no pone el job en rojo) 2026-11-04 14:14:44 +01:00
docs feat: sidepanel-first UI pivot + CI ZIP by repo name + fleet MV3 rules 2026-09-15 21:44:45 +00:00
protocols Diseño: contrato DESIGN.md, pie canónico y cero ko-fi 2026-10-16 00:25:45 +02:00
public feat: sidepanel-first UI pivot + CI ZIP by repo name + fleet MV3 rules 2026-09-15 21:44:45 +00:00
scripts chore: add check-sidepanel gate to validate 2026-10-24 15:10:40 +02:00
src canon oct-2026: pie de 2 iconos con feedback?ext, version unica y gate del ZIP 2026-10-23 21:46:45 +02:00
tests feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
.gitignore feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
biome.json feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
commitlint.config.cjs feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
DESIGN.md Diseño: contrato DESIGN.md, pie canónico y cero ko-fi 2026-10-16 00:25:45 +02:00
lefthook.yml feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
LICENSE feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
package.json chore: add check-sidepanel gate to validate 2026-10-24 15:10:40 +02:00
pnpm-lock.yaml feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
pnpm-workspace.yaml feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
README.md canon oct-2026: pie de 2 iconos con feedback?ext, version unica y gate del ZIP 2026-10-23 21:46:45 +02:00
tsconfig.json feat: extension skeleton template (canonical footer, CI, i18n, cws-listing) 2026-09-05 16:15:28 +02:00
wxt.config.ts canon oct-2026: pie de 2 iconos con feedback?ext, version unica y gate del ZIP 2026-10-23 21:46:45 +02:00

Greasy Sprocket — Extension Skeleton Template

Base para TODAS las extensiones de Greasy Sprocket. Clona este repo, renombra y rellena los placeholders. Así el footer, CI, listing y hooks de calidad son idénticos en toda la flota y un fix se propaga copiando el fichero canónico.

Fleet defaults (17-sep-2026, lecciones SessionVault v0.1.1→v0.1.4)

  • Sidepanel es el UI por defecto: setPanelBehavior({openPanelOnActionClick:true}) en top-level del background (NO dentro de onClicked) + setUninstallURL. NUNCA crear entrypoint popup/ — WXT inyecta default_popup y sombreada el panel.
  • Permisos base en wxt.config.ts: sidePanel + storage + alarms (ya descomentados). Si la ext no usa alarms, BORRA el permiso pero TAMBIÉN borra cualquier uso de browser.alarms — usarlo sin permiso mata el SW: "Service worker registration failed (code 15)".
  • Versión: una sola fuente, package.json. No escribas un literal en wxt.config.ts: WXT toma la versión de package.json y scripts/check-manifest-version.sh comprueba que el manifest DENTRO del ZIP coincide con ella. Dos literales = dos verdades.
  • i18n completo en _locales (en/es): toda clave usada en data-i18n debe existir en messages.json o el texto sale vacío.

Cómo crear una extensión nueva

# 1. Crear repo vacío en Gitea (API) y clonarlo
git clone https://git.greasysprocket.com/monyi/<ext-id>.git
cd <ext-id>

# 2. Copiar el esqueleto (sin .git del template)
cp -r ~/gs-work/ext-template/{.gitea,docs,public,src,tests,biome.json,commitlint.config.cjs,lefthook.yml,package.json,pnpm-workspace.yaml,tsconfig.json,.gitignore} .

# 3. Renombrar placeholders (ver SECTIONS abajo)
grep -rn "EXT_ID_HERE\|EXT_NAME_HERE\|EXT_DESCRIPTION_HERE" .

# 4. pnpm install && pnpm wxt prepare && pnpm run validate
# 5. Primer commit + push

Placeholders a rellenar

Placeholder Dónde Ejemplo (tabsnooze)
EXT_ID_HERE footer ☕ (?ext=), storage keys, ci.yml ya usa el nombre del repo sessionvault
EXT_NAME_HERE manifest name, footer title, listing TabSnooze
EXT_DESCRIPTION_HERE package.json + wxt.config.ts + listing "Snooze tabs for later..."
EXT_PERMISSIONS_HERE wxt.config.ts (solo las que uses, con justificación en listing) ["alarms","tabs","storage","contextMenus"]

Qué trae el esqueleto

  • UI sidepanel por defecto (sin popup): src/entrypoints/sidepanel/ con footer canónico, theme toggle persistido, side_panel.default_path, permiso sidePanel y minimum_chrome_version: 114. El icono abre el panel vía setPanelBehavior (fallback open fire-open-then-flag en background).

  • Footer canónico (inline en src/entrypoints/sidepanel/index.html + bloque .gs-footer* en su style.css): 💬 Support / 🐛 Feedback / ☕ Support ext / Greasy Sprocket. IGUAL en todas las extensiones — solo cambia el ?ext= del ☕. Si se arregla algo aquí, se copia a la flota.

  • CI Gitea Actions (.gitea/workflows/ci.yml): validate (typecheck+lint+test+build) en push a main + release job que sube el ZIP a la Release en tags v*. Requiere runner activo en la instancia.

  • i18n scaffold: public/_locales/en/messages.json + es/messages.json (14 claves estándar del footer y popup genérico) y default_locale: "en" ya en el manifest — sin esto Chrome rechaza la carga.

  • docs/cws-listing.md plantilla: single purpose, justificación de cada permiso, descripciones, checkboxes de privacidad.

  • docs/screenshots/ — vacío, para las 3 capturas 1280×800 (receta: references/cws-screenshots del skill browser-extension-publish).

  • Calidad: Biome + commitlint + lefthook (mismo config que tabsnooze).

  • Tests: Vitest con ejemplo de test de lógica pura en tests/.

Reglas de flota (no negociables)

  1. UI por defecto = sidepanel (decisión flota 15-sep-2026). Popup solo como stub de compatibilidad. El icono abre el panel vía setPanelBehavior (y fallback sidePanel.open fire-open-then-flag). CSS del panel: body { width: 100% }, #app { height: 100vh; display:flex; flex-direction:column }, contenido flex:1 — NUNCA ancho fijo 320px.

  2. Sin nag screens. Nada se abre solo, nunca. El apoyo es el icono de support del footer (pasivo: solo abre la página si el usuario lo pulsa).

  3. Footer idéntico en todas las extensiones (este template es la fuente de verdad): dos iconos —/support (sin parámetro) y /feedback?ext=<slug>— más el enlace de texto a greasysprocket.com. El ?ext= va SOLO en feedback: /support no lo lee, y el formulario lo necesita para atribuir la respuesta. El slug debe existir en ext-ids.ts del sitio.

  4. Sin ko-fi ni URLs externas en el footer: solo greasysprocket.com.

  5. default_locale + _locales/ van juntos — siempre los dos o ninguno.

  6. Permisos mínimos: solo los que el listing justifica. <all_urls> solo con decisión explícita de producto (jsonifier lo usa para detectar JSON servido con cualquier URL) y asumiendo el aviso de la store: "leer y cambiar todos tus datos en todos los sitios".

  7. Manifest version = tag version = Release version. El publicador (gs-publish-release.py) rechaza publicar si tag != package.json.version o si el manifest del ZIP no coincide.

  8. Pre-submit: seguir el checklist del skill browser-extension-publish (incluye verificar el ZIP descargado de la Release, no solo el build local).

  9. Cada extensión tiene su propia privacy policy en la web (página /privacy por extensión o sección dedicada).

Vive en src/entrypoints/sidepanel/ (index.html + style.css). Requiere que el tema defina --border y --text-muted.

<footer class="gs-footer">
  <a href="https://greasysprocket.com/support" target="_blank" rel="noopener"
     class="gs-footer-icon" title="Support" aria-label="Support"><svg aria-hidden="true" ...>…</svg></a>
  <a href="https://greasysprocket.com/feedback?ext=EXT_SLUG_HERE" target="_blank" rel="noopener"
     class="gs-footer-icon" title="Feedback" aria-label="Feedback"><svg aria-hidden="true" ...>…</svg></a>
  <a href="https://greasysprocket.com" target="_blank" rel="noopener" class="gs-footer-text">Greasy Sprocket</a>
</footer>

Un solo enlace a /support: el icono de taza de ko-fi se retiró en oct-2026 porque duplicaba Support y su ?ext= no lo leía nadie. El gate E5 falla si hay dos support o si falta feedback?ext=.

Release

# 1. subir la version en package.json (patch/minor segun el cambio)
pnpm validate            # typecheck + lint + tests
bash scripts/check-design.sh
pnpm build
bash scripts/build-zip.sh            # -> dist/<repo>-<version>-chrome.zip + gate del manifest

# 2. publicar (valida tag == package.json.version y el manifest del ZIP)
python3 ~/work/infra/bin/gs-publish-release.py . --zip dist/<repo>-<version>-chrome.zip

# 3. verificar por la ruta real del navegador
curl -s https://greasysprocket.com/updates/<repo>.xml     # version + codebase nuevos

Sin bump de version no hay release: es la causa de que la flota sirviera builds de agosto mientras main tenia meses de trabajo.

Reglas MV3 (de la doc oficial developer.chrome.com, aprendidas en la flota)

  1. Listeners síncronos top-level: el SW MV3 se descarga al quedar inactivo; todos los addListener van en el scope global de defineBackground, nunca tras un await.
  2. Timers → chrome.alarms: setTimeout/setInterval mueren con el SW (auto-lock, polling). Persistir el estado en storage para re-armar tras el wake.
  3. onMessage con respuesta async: return true explícito en el listener (o promesa desde Chrome 148 — NO usar como único camino hasta que el rollout sea completo).
  4. sidePanel.open() es gesture-gated: JAMÁS un await antes de open(). Puede lanzar EXCEPCIÓN SÍNCRONA si no hay tabId — try/catch, no solo .catch. Fallbacks: tabId → windowId → setPanelBehavior({openPanelOnActionClick:true}).
  5. minimum_chrome_version: declarar si se usan APIs recientes (sidePanel → "114").
  6. i18n: default_locale + _locales/ juntos siempre; cero claves EN sin traducir en el texto ES (grep visual antes de taggear).