Contexto
Cada módulo estático (static/js/**/*.js) se servía con caché larga gracias al hash que Django (collectstatic + Whitenoise) añade al nombre del fichero final. Pero eso solo cubre la URL que pide la plantilla con {% static %}: los propios módulos JS se importan entre sí con rutas relativas (import { x } from './y.js?v=7'), y ese número de versión lo subía una persona a mano en cada cambio.
Dos formas de fallo ya sufridas por el proyecto (recogidas en la regla de diseño .claude/rules/ui-design.md y en la regla global diseno.md):
- Olvidar subir el
?v=de un módulo tras editarlo: el navegador de un usuario que ya tenía la página abierta sigue ejecutando la versión vieja del módulo. - Subir el número en un sitio y no en otro: dos
?v=distintos para el mismo fichero son dos URLs distintas para el navegador, que las trata como dos módulos — código duplicado ejecutándose dos veces (por ejemplo, un botón que crea el elemento dos veces).
La mega-auditoría de septiembre (tanda T34, hallazgo C2-calidad-frontend-02 / E2-rendimiento-frontend-agente-08) señaló esto como deuda: 42 apariciones de ?v= puestas a mano en 7 plantillas y 544 imports relativos dentro de static/js sin ningún mecanismo automático.
Opciones consideradas
- Seguir con el
?v=manual, pero con una checklist o un test que obligue a subirlo en cada PR que toque un módulo. Descartada: ya existía la advertencia en las reglas del proyecto y el fallo se repitió; un proceso manual con disciplina humana no cierra el problema de raíz. - Usar el mecanismo de serie de Django (
support_js_module_import_aggregation, experimental) para quecollectstaticreescriba los imports. Descartada tal cual: probado contra el árbol real destatic/js, sus patrones casan con la palabraimport/exportdentro de comentarios (reescriben de más o apuntan a ficheros que no existen), no entienden que la ruta ya trae un?v=N(calculan el hash sobre el contenido sin quitarlo, y el import queda apuntando a un fichero que no existe), y la cadena de imports más larga del proyecto (9 módulos) supera el tope de 5 pasadas que trae Django por defecto. - Patrones propios sobre el mismo mecanismo de Django, ajustados a lo que de verdad hay en el repo (ver Decisión).
Decisión elegida
Opción 3. core/storage.py (ForgivingManifestStaticFilesStorage) define sus propios patrones de reescritura para collectstatic:
- Solo reescribe sentencias
import/export … fromque empiezan de línea o van justo detrás de un;(para código minificado en una sola línea) — así unaimportmencionada dentro de un comentario (que empieza por*o//) no casa. - El especificador se reescribe sin su
?v=…: el hash del fichero ya hace de versión, así que el import y la etiqueta{% static %}de la plantilla acaban apuntando a la misma URL — un módulo, una sola ejecución. max_post_process_passessube de 5 (valor de Django) a 20: medido contra el árbol real, la cadena de imports necesita 4-6 pasadas según el orden en que el sistema de archivos entrega los ficheros, y el peor orden posible con la cadena más larga (9 módulos) pediría 8.- El “perdón” que antes existía para cualquier
MissingFileError(dejar pasar el build cuando unurl()de un CSS de terceros no se puede resolver) se restringe a CSS y a un sourcemap.mapausente. En un JS, un import que apunta a un fichero inexistente aborta el build — antes ese perdón habría dejado pasar en silencio una versión con imports sin hash.
Como consecuencia, los 42 ?v= sueltos en las 7 plantillas que los tenían se retiraron, y la regla del proyecto (.claude/rules/ui-design.md) pasa de “sube el ?v= a mano” a “el nombre con hash es la versión, no hay nada que subir a mano”.
Consecuencias
A favor: se cierra de raíz la clase de fallo que motivó esta decisión — no hay número que alguien pueda olvidar subir, y la caché de un módulo se renueva sola en toda la cadena de imports que lo usa (el hash de quien importa cambia cuando cambia el hash de lo importado). Un test dedicado (tests/test_mega25_r7estaticos_imports.py) pasa el post-procesado real sobre todo static/js y falla si algún import queda sin reescribir, apunta a un fichero inexistente, conserva una query o la cadena se acerca al tope de pasadas.
En contra / riesgo asumido: el post-procesador ya no es el comportamiento de serie de Whitenoise/Django, sino expresiones regulares propias mantenidas en core/storage.py — cualquier patrón de import JS nuevo que el proyecto empiece a usar (por ejemplo, import dinámico con plantillas de string) necesita revisarse contra estos patrones. Un import roto en un JS ahora rompe el build entero de estáticos en vez de degradarse en silencio; es la consecuencia buscada, pero hay que tenerlo presente al depurar un collectstatic que falla.
Status: accepted (mergeado a main en v1.162.0).
Véase también
- [[entity—functions—endpoint—tools-deps]]