From 948056d0bff46fec0d453ba63427d672087228b6 Mon Sep 17 00:00:00 2001 From: Allan Lasser Date: Fri, 2 Jan 2026 17:52:19 -0500 Subject: [PATCH 01/13] Replaces Webpack with Vite --- .gitignore | 2 + compose/local/vite.Dockerfile | 10 + local.yml | 24 + muckrock/assets/entry.js | 13 +- muckrock/assets/js/muckrock.js | 4 +- muckrock/settings/base.py | 10 +- muckrock/settings/heroku.py | 8 + muckrock/settings/local.py | 11 + muckrock/templates/base.html | 32 +- muckrock/templates/foia/file/embed.html | 4 +- npm-shrinkwrap.json | 10387 ++++++---------------- package.json | 33 +- pip/requirements.txt | 2 +- vite.config.js | 48 + 14 files changed, 2780 insertions(+), 7808 deletions(-) create mode 100644 compose/local/vite.Dockerfile create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore index 9e0451df7..905311f35 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ bundles style.*.css .vagrant node_modules +muckrock/assets/dist .settings.sh webpack-stats.json npm-debug.log @@ -38,3 +39,4 @@ celerybeat-schedule CLAUDE.md /data .env +.claude diff --git a/compose/local/vite.Dockerfile b/compose/local/vite.Dockerfile new file mode 100644 index 000000000..63f7ccc3c --- /dev/null +++ b/compose/local/vite.Dockerfile @@ -0,0 +1,10 @@ +FROM node:22 AS base + +# Set working directory +WORKDIR /app + +# Install node modules +COPY package*.json ./ +RUN npm install --include=dev --legacy-peer-deps --ignore-scripts + +# Note: vite.config.js and src code will be mounted via volumes diff --git a/local.yml b/local.yml index b322294be..20cd47ddc 100644 --- a/local.yml +++ b/local.yml @@ -23,6 +23,8 @@ services: env_file: - ./.envs/.local/.django - ./.envs/.local/.postgres + ports: + - "8000:80" command: /start networks: default: @@ -76,6 +78,28 @@ services: squarelet_default: aliases: [] + muckrock_vite: + build: + context: . + dockerfile: ./compose/local/vite.Dockerfile + image: muckrock_local_vite + restart: always + ports: + - "4201:4200" + command: "npm run dev" + volumes: + - ".:/app" + - "/app/node_modules" + networks: + default: + aliases: + - internal.dev.muckrock.com + - internal.dev.foiamachine.org + squarelet_default: + aliases: + - internal.dev.muckrock.com + - internal.dev.foiamachine.org + networks: squarelet_default: external: true diff --git a/muckrock/assets/entry.js b/muckrock/assets/entry.js index feef1de00..3d9fa481d 100644 --- a/muckrock/assets/entry.js +++ b/muckrock/assets/entry.js @@ -1,3 +1,12 @@ -import 'core-js/stable'; -import './js/muckrock.js' +// Vite module preload polyfill +import "vite/modulepreload-polyfill"; + +// Make jQuery available globally for legacy code FIRST +import jQuery from "jquery"; +window.$ = window.jQuery = jQuery; + +// Import styles (doesn't depend on jQuery) import './scss/style.scss' + +// Use dynamic import to ensure jQuery is set up before muckrock.js loads +import('./js/muckrock.js') diff --git a/muckrock/assets/js/muckrock.js b/muckrock/assets/js/muckrock.js index 6611b61b2..b6f26123e 100644 --- a/muckrock/assets/js/muckrock.js +++ b/muckrock/assets/js/muckrock.js @@ -20,8 +20,8 @@ import '../vendor/formset'; import '../vendor/loupe'; import '../vendor/quicksearch'; import '../vendor/stacktable'; -import qq from '../vendor/fine-uploader'; -window.qq = qq; +import '../vendor/fine-uploader'; // UMD module, attaches to window.qq +// Note: autocomplete_light scripts are loaded via Django static files in templates import './account'; import './checkout'; diff --git a/muckrock/settings/base.py b/muckrock/settings/base.py index 2d45f32a8..9156ece54 100644 --- a/muckrock/settings/base.py +++ b/muckrock/settings/base.py @@ -94,12 +94,8 @@ def boolcheck(setting): STATICFILES_DIRS = (os.path.join(SITE_ROOT, "assets"),) -WEBPACK_LOADER = { - "DEFAULT": { - "BUNDLE_DIR_NAME": "bundles/", - "STATS_FILE": os.path.join(SITE_ROOT, "assets/webpack-stats.json"), - } -} +# DJANGO_VITE configuration is defined in environment-specific settings +# (local.py, production.py, etc.) to properly reference DEBUG COMPRESS_OFFLINE = True @@ -293,7 +289,7 @@ def boolcheck(setting): "storages", "taggit", "watson", - "webpack_loader", + "django_vite", "django_hosts", "hijack", "django_filters", diff --git a/muckrock/settings/heroku.py b/muckrock/settings/heroku.py index 473fffff9..382a63eb6 100644 --- a/muckrock/settings/heroku.py +++ b/muckrock/settings/heroku.py @@ -19,6 +19,14 @@ # MuckRock from muckrock.settings.base import * +# Vite configuration for production +DJANGO_VITE = { + "default": { + "dev_mode": False, + "manifest_path": os.path.join(SITE_ROOT, "assets/dist/manifest.json"), + } +} + INSTALLED_APPS = ("scout_apm.django",) + INSTALLED_APPS USE_SCOUT = True diff --git a/muckrock/settings/local.py b/muckrock/settings/local.py index 8b92dc009..b3f5829f5 100644 --- a/muckrock/settings/local.py +++ b/muckrock/settings/local.py @@ -10,6 +10,17 @@ DEBUG = True +# Vite configuration for local development +DJANGO_VITE = { + "default": { + "dev_mode": DEBUG, + "dev_server_protocol": "http", + "dev_server_host": "localhost", + "dev_server_port": 4201, + "manifest_path": os.path.join(SITE_ROOT, "assets/dist/manifest.json"), + } +} + # Loads static files locally STORAGES["staticfiles"][ "BACKEND" diff --git a/muckrock/templates/base.html b/muckrock/templates/base.html index 2fa1c24a2..0ec5d3938 100644 --- a/muckrock/templates/base.html +++ b/muckrock/templates/base.html @@ -1,6 +1,6 @@ {% load static %} {% load compress %} -{% load render_bundle from webpack_loader %} +{% load django_vite %} {% load hijack_tags %} {% load opensearch_tags %} @@ -62,8 +62,8 @@ - {% render_bundle 'main' 'css' %} {% endcompress %} + {% vite_asset 'muckrock/assets/entry.js' %} {% block styles %} {% endblock %} @@ -215,12 +215,28 @@ {% block prescripts %} {% endblock prescripts %} - {% compress js %} - {% render_bundle 'main' 'js' %} - - - - {% endcompress %} + + {% block scripts %} {% endblock scripts %} diff --git a/muckrock/templates/foia/file/embed.html b/muckrock/templates/foia/file/embed.html index a70bf32ff..ef2a0b99f 100644 --- a/muckrock/templates/foia/file/embed.html +++ b/muckrock/templates/foia/file/embed.html @@ -1,5 +1,5 @@ {% load compress %} -{% load render_bundle from webpack_loader %} +{% load django_vite %} {% load static %} {% with file=object %} @@ -8,7 +8,7 @@ {{file.title}} • MuckRock {% compress css %} - {% render_bundle 'docViewer' 'css' %} + {% vite_asset 'muckrock/assets/js/docViewer.js' %} - - - - - {% endcompress %} + + + + {% vite_asset 'muckrock/assets/entry.js' %} {% block styles %} {% endblock %} diff --git a/muckrock/templates/foia/detail.html b/muckrock/templates/foia/detail.html index 44c0184a7..5c1372941 100644 --- a/muckrock/templates/foia/detail.html +++ b/muckrock/templates/foia/detail.html @@ -1,5 +1,4 @@ {% extends "base.html" %} -{% load compress %} {% load humanize %} {% load mathfilters %} {% load static %} @@ -62,7 +61,7 @@ {% crowdfund foia.crowdfund.pk %} {% endif %} - {% compress_cache foia_cache_timeout foia_detail_bottom foia.pk request.user.pk %} + {% cache foia_cache_timeout foia_detail_bottom foia.pk request.user.pk %} {% include "foia/detail/actions.html" %} diff --git a/muckrock/templates/foia/file/embed.html b/muckrock/templates/foia/file/embed.html index ef2a0b99f..dc9f6c341 100644 --- a/muckrock/templates/foia/file/embed.html +++ b/muckrock/templates/foia/file/embed.html @@ -1,4 +1,3 @@ -{% load compress %} {% load django_vite %} {% load static %} {% with file=object %} @@ -7,25 +6,7 @@ {{file.title}} • MuckRock - {% compress css %} {% vite_asset 'muckrock/assets/js/docViewer.js' %} - - {% endcompress %}
diff --git a/pip/requirements.in b/pip/requirements.in index af4358ee3..3d31363a7 100644 --- a/pip/requirements.in +++ b/pip/requirements.in @@ -13,7 +13,6 @@ django-anymail[mailgun] # Use for sending email on production django-autocomplete-light==3.9.0rc5 # Autocomplete drop down inputs django-celery-email # Send emails asynchronously using Celery django-choices # Define choices for a field -django-compressor # Compress all JS and CSS assets into a single file django-constance[database] # Use for dynamic settings django-cors-headers # Allows access to API endpoints from non-MuckRock domains django-debug-toolbar # Toolbar for debugging @@ -34,8 +33,8 @@ django-simple-history # Jurisdiction page history django-sslify # Force SSL everywhere django-storages # Store files on S3 django-taggit # Used for tagging +django-vite # Used for loading Vite bundles django-watson # Used for search -django-webpack-loader # Used for loading Webpack bundles django<5.0 # Django is kinda important... djangorestframework # The API framework djangorestframework_simplejwt # JWT auth for DRF diff --git a/pip/requirements.txt b/pip/requirements.txt index 502e73841..1bac685ff 100644 --- a/pip/requirements.txt +++ b/pip/requirements.txt @@ -155,6 +155,7 @@ django==4.2 # django-sslify # django-storages # django-taggit + # django-vite # djangorestframework # djangorestframework-simplejwt # dogslow @@ -170,7 +171,6 @@ django-anymail[mailgun]==9.1 django-appconf==1.0.4 # via # django-celery-email - # django-compressor # django-opensearch django-autocomplete-light==3.9.0rc5 # via -r pip/requirements.in @@ -178,8 +178,6 @@ django-celery-email==3.0.0 # via -r pip/requirements.in django-choices==1.6.1 # via -r pip/requirements.in -django-compressor==4.3.1 - # via -r pip/requirements.in django-constance[database]==2.9.1 # via -r pip/requirements.in django-cors-headers==3.4.0 @@ -224,10 +222,10 @@ django-storages==1.12.3 # via -r pip/requirements.in django-taggit==3.1.0 # via -r pip/requirements.in -django-watson==1.5.5 - # via -r pip/requirements.in django-vite==3.0.4 # via -r pip/requirements.in +django-watson==1.5.5 + # via -r pip/requirements.in djangorestframework==3.14.0 # via # -r pip/requirements.in @@ -497,8 +495,6 @@ pyyaml==6.0.1 # python-documentcloud ratelimit==2.2.1 # via python-documentcloud -rcssmin==1.1.1 - # via django-compressor redis==3.5.3 # via # -r pip/requirements.in @@ -534,8 +530,6 @@ requests[security]==2.31.0 # zenpy requests-oauthlib==1.0.0 # via social-auth-core -rjsmin==1.2.1 - # via django-compressor rpds-py==0.18.0 # via # jsonschema From 8f6aceb18f89460ddb20dbcbcdee5b3ea5013f44 Mon Sep 17 00:00:00 2001 From: Allan Lasser Date: Thu, 8 Jan 2026 17:23:50 -0500 Subject: [PATCH 05/13] Update storage.py --- muckrock/core/storage.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/muckrock/core/storage.py b/muckrock/core/storage.py index 8920c1000..f2a8587cb 100644 --- a/muckrock/core/storage.py +++ b/muckrock/core/storage.py @@ -1,10 +1,10 @@ """ -Cache classes that extend S3, for asset compression +Storage classes that extend S3 """ # Django from django.conf import settings -from django.core.files.storage import get_storage_class +from django.contrib.staticfiles.storage import StaticFilesStorage # Third Party from storages.backends.s3boto3 import S3Boto3Storage @@ -14,32 +14,25 @@ class CachedS3Boto3Storage(S3Boto3Storage): """ - S3 storage backend for static files that saves the files locally, too. + S3 storage backend for static files that also saves files locally. """ bucket_name = settings.AWS_STORAGE_BUCKET_NAME + default_acl = settings.AWS_DEFAULT_ACL def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.local_storage = get_storage_class( - "compressor.storage.CompressorFileStorage" - )() + self.local_storage = StaticFilesStorage() def save(self, name, content, max_length=None): + # Save to local storage first # pylint: disable=protected-access self.local_storage._save(name, content) + # Then save to S3 super().save(name, self.local_storage._open(name), max_length) return name -class OfflineManifestFileStorage(CachedS3Boto3Storage): - """Store into the COMPRESS_OUTPUT_DIR""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.location = settings.COMPRESS_OUTPUT_DIR - - class MediaRootS3BotoStorage(S3Boto3Storage): """ S3 storage backend for user-uploaded media files. From 77c88cb667d5db45bc4e383bcee22a757ff8ce82 Mon Sep 17 00:00:00 2001 From: Allan Lasser Date: Fri, 9 Jan 2026 14:01:26 -0500 Subject: [PATCH 06/13] Storage settings tweak --- muckrock/core/storage.py | 3 ++- muckrock/settings/base.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/muckrock/core/storage.py b/muckrock/core/storage.py index f2a8587cb..e2ed32c0c 100644 --- a/muckrock/core/storage.py +++ b/muckrock/core/storage.py @@ -15,10 +15,11 @@ class CachedS3Boto3Storage(S3Boto3Storage): """ S3 storage backend for static files that also saves files locally. + Files inherit the bucket's public access policy rather than setting per-object ACLs. + This works with modern AWS S3 security settings that block public ACLs. """ bucket_name = settings.AWS_STORAGE_BUCKET_NAME - default_acl = settings.AWS_DEFAULT_ACL def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/muckrock/settings/base.py b/muckrock/settings/base.py index adeb6aeef..99f3ea936 100644 --- a/muckrock/settings/base.py +++ b/muckrock/settings/base.py @@ -130,7 +130,14 @@ def boolcheck(setting): "Expires": "Thu, 31 Dec 2099 20:00:00 GMT", "Cache-Control": "max-age=94608000", } +# Note: AWS_DEFAULT_ACL is kept for backwards compatibility with other storage classes +# but CachedS3Boto3Storage overrides this to None to work with modern S3 security AWS_DEFAULT_ACL = os.environ.get("AWS_STORAGE_DEFAULT_ACL", "public-read") +# Modern way to set object parameters for S3 +# Works with AWS S3 buckets that have "Block Public Access" enabled +AWS_S3_OBJECT_PARAMETERS = { + "CacheControl": "max-age=94608000", +} AWS_S3_MAX_MEMORY_SIZE = int(os.environ.get("AWS_S3_MAX_MEMORY_SIZE", 16 * 1024 * 1024)) AWS_S3_MIN_PART_SIZE = int(os.environ.get("AWS_S3_MIN_PART_SIZE", 16 * 1024 * 1024)) From d23d4692d5b890e88cf000285c90b696a9ee05ed Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Fri, 13 Feb 2026 13:39:01 -0500 Subject: [PATCH 07/13] vite configuration --- Procfile | 2 +- vite.config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Procfile b/Procfile index 8e6d1a72e..7445b7750 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ web: bin/start-nginx gunicorn -c config/gunicorn.conf muckrock.wsgi:application scheduler: celery -A muckrock.core.celery worker -E -B --loglevel=INFO worker: celery -A muckrock.core.celery worker -E -Q celery,phaxio --loglevel=INFO -release: python manage.py compress --settings=muckrock.settings.compress_production && python manage.py migrate --no-input && python manage.py collectstatic --no-input +release: python manage.py migrate --no-input && python manage.py collectstatic --no-input diff --git a/vite.config.js b/vite.config.js index c054ee9cf..620326f85 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,7 +9,7 @@ export default defineConfig({ base: "/static/", build: { manifest: "manifest.json", - outDir: path.resolve(__dirname, "muckrock/assets/dist"), + outDir: path.resolve(__dirname, "muckrock/assets"), rollupOptions: { input: { main: path.resolve(__dirname, "muckrock/assets/entry.js"), From 18f46f0da3250e7ef4b11aa75c6d5df59d6428ea Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Fri, 13 Feb 2026 13:50:22 -0500 Subject: [PATCH 08/13] vite configuration --- vite.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.js b/vite.config.js index 620326f85..830fb3723 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,7 +9,7 @@ export default defineConfig({ base: "/static/", build: { manifest: "manifest.json", - outDir: path.resolve(__dirname, "muckrock/assets"), + outDir: path.resolve(__dirname, "muckrock/assets/dist"), rollupOptions: { input: { main: path.resolve(__dirname, "muckrock/assets/entry.js"), @@ -26,7 +26,7 @@ export default defineConfig({ }, resolve: { alias: { - "@": path.resolve(__dirname, "muckrock/assets"), + "@": path.resolve(__dirname, "muckrock/assets/dist"), }, extensions: [".js", ".jsx", ".json"], }, From 45aa43977a170f5b8961b820eef92057a91c5a73 Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Fri, 13 Feb 2026 15:56:21 -0500 Subject: [PATCH 09/13] restore compress_cache tag --- muckrock/assets/scss/base/_fonts.scss | 46 --------------------------- muckrock/templates/foia/detail.html | 2 +- 2 files changed, 1 insertion(+), 47 deletions(-) delete mode 100644 muckrock/assets/scss/base/_fonts.scss diff --git a/muckrock/assets/scss/base/_fonts.scss b/muckrock/assets/scss/base/_fonts.scss deleted file mode 100644 index 4396ceb1e..000000000 --- a/muckrock/assets/scss/base/_fonts.scss +++ /dev/null @@ -1,46 +0,0 @@ -/* -** _fonts.scss -** -** Font-face declarations for Source Sans Pro and Source Code Pro -** Note: Font files are served from Django's static directory, not bundled by Vite -*/ - -@font-face { - font-family: 'Source Sans Pro'; - src: url('/static/fonts/SourceSansPro-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Source Sans Pro'; - src: url('/static/fonts/SourceSansPro-Italic.ttf') format('truetype'); - font-weight: 400; - font-style: italic; - font-display: swap; -} - -@font-face { - font-family: 'Source Sans Pro'; - src: url('/static/fonts/SourceSansPro-Semibold.ttf') format('truetype'); - font-weight: 500 600; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Source Sans Pro'; - src: url('/static/fonts/SourceSansPro-SemiboldItalic.ttf') format('truetype'); - font-weight: 500 600; - font-style: italic; - font-display: swap; -} - -@font-face { - font-family: 'Source Code Pro'; - src: url('/static/fonts/SourceCodePro-Regular.ttf') format('truetype'); - font-weight: normal; - font-style: normal; - font-display: swap; -} diff --git a/muckrock/templates/foia/detail.html b/muckrock/templates/foia/detail.html index 5c1372941..dbb43ccc4 100644 --- a/muckrock/templates/foia/detail.html +++ b/muckrock/templates/foia/detail.html @@ -61,7 +61,7 @@ {% crowdfund foia.crowdfund.pk %} {% endif %} - {% cache foia_cache_timeout foia_detail_bottom foia.pk request.user.pk %} + {% compress_cache foia_cache_timeout foia_detail_bottom foia.pk request.user.pk %} {% include "foia/detail/actions.html" %} From dbebc9c0a6428180c4176c768cad63466eecd552 Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Fri, 13 Feb 2026 15:56:35 -0500 Subject: [PATCH 10/13] move font loading to base html --- muckrock/assets/scss/base/_base.scss | 1 - muckrock/templates/base.html | 35 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/muckrock/assets/scss/base/_base.scss b/muckrock/assets/scss/base/_base.scss index a54258dd3..94b3b4bac 100644 --- a/muckrock/assets/scss/base/_base.scss +++ b/muckrock/assets/scss/base/_base.scss @@ -8,5 +8,4 @@ @import 'mixins'; @import 'colors'; @import 'grid'; -@import 'fonts'; @import 'typography'; diff --git a/muckrock/templates/base.html b/muckrock/templates/base.html index 9580df70e..2be9d3776 100644 --- a/muckrock/templates/base.html +++ b/muckrock/templates/base.html @@ -26,6 +26,41 @@ + {% vite_asset 'muckrock/assets/entry.js' %} {% block styles %} {% endblock %} From c26f9afd67714258a6d57846895fcec15c5d0ef8 Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Fri, 13 Feb 2026 16:18:29 -0500 Subject: [PATCH 11/13] vite configuration --- vite.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.js b/vite.config.js index 830fb3723..2567b30fe 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export default defineConfig({ - base: "/static/", + base: "/static/dist/", build: { manifest: "manifest.json", outDir: path.resolve(__dirname, "muckrock/assets/dist"), @@ -26,7 +26,7 @@ export default defineConfig({ }, resolve: { alias: { - "@": path.resolve(__dirname, "muckrock/assets/dist"), + "@": path.resolve(__dirname, "muckrock/assets"), }, extensions: [".js", ".jsx", ".json"], }, From 0d8db7173b211da4306daf044a35823801cde977 Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Fri, 13 Feb 2026 16:43:19 -0500 Subject: [PATCH 12/13] vite configuration --- vite.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.js b/vite.config.js index 2567b30fe..c6ecdc771 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,6 +10,7 @@ export default defineConfig({ build: { manifest: "manifest.json", outDir: path.resolve(__dirname, "muckrock/assets/dist"), + assetsDir: "", rollupOptions: { input: { main: path.resolve(__dirname, "muckrock/assets/entry.js"), From 5bdb9247ea9f26781761d3b636701153d2aa1aa5 Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Fri, 13 Feb 2026 16:48:59 -0500 Subject: [PATCH 13/13] vite configuration --- vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.js b/vite.config.js index c6ecdc771..fabc9fe21 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export default defineConfig({ - base: "/static/dist/", + base: "dist/", build: { manifest: "manifest.json", outDir: path.resolve(__dirname, "muckrock/assets/dist"),