diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..d6613ebc0169817a4a61bd626f64d8a0a6867a6e --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +#!/usr/bin/make -f + +PYTHON = python +VENV = .venv +PORT = 8012 +SETTINGS = registry.settings.dev + +.PHONY: help venv install build run shell migrations migrate + +help: + @echo "Setup:" + @echo " venv Setup virtual environment" + @echo " install Install dependencies to venv" + @echo " build Build CSS and JS files" + @echo "" + @echo "Application:" + @echo " run Run the application on port ${PORT}" + @echo " shell Access the Django shell" + @echo "" + @echo "Database:" + @echo " migrations Generate migrations" + @echo " migrate Run migrations" + +venv: .venv/bin/python +.venv/bin/python: + ${PYTHON} -m venv ${VENV} + +install: venv + ${VENV}/bin/pip install -r requirements/base.txt -r requirements/production.txt + ${VENV}/bin/nodeenv --python-virtualenv --node=19.3.0 + ${VENV}/bin/npm install + + +build: venv + ${VENV}/bin/npm run build + ${VENV}/bin/python manage.py collectstatic --noinput --settings=${SETTINGS} + +run: venv + ${VENV}/bin/python manage.py runserver ${PORT} --settings=${SETTINGS} + +shell: venv + ${VENV}/bin/python manage.py shell --settings=${SETTINGS} + +migrations: venv + ${VENV}/bin/python manage.py makemigrations --settings=${SETTINGS} + +migrate: venv + ${VENV}/bin/python manage.py migrate --settings=${SETTINGS} diff --git a/env.example b/env.example index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f3ee301e15c0806c42e59a42f774f1ae9ee082db 100644 --- a/env.example +++ b/env.example @@ -0,0 +1 @@ +DEFAULT_SIGNING_PARTY_REPRESENTATIVE="Česká pirátská strana\nNa Moráni 360/3\n128 00 Praha 2\nIČ: 71339698\nDIČ: CZ71339698" diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..5675a2e01fbf86888a2003cc1a1bebc8438336c6 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "contract-registry", + "version": "0.0.1", + "description": "", + "scripts": { + "build": "npx tailwindcss -i ./static_src/base.css -o ./shared/static/shared/style.css && npx webpack build" + }, + "repository": { + "type": "git", + "url": "git@gitlab.pirati.cz:to/contract-registry.git" + }, + "author": "Tomáš Valenta", + "license": "AGPL-3.0-or-later", + "dependencies": { + "css-loader": "^6.7.3", + "jquery": "^3.6.3", + "tailwindcss": "^3.2.4", + "webpack": "^5.75.0", + "webpack-bundle-tracker": "^1.8.0", + "webpack-cli": "^5.0.1" + } +} diff --git a/registry/manage.py b/registry/manage.py new file mode 100755 index 0000000000000000000000000000000000000000..0e1442be86443cb560d236a8317bc4f0f1416563 --- /dev/null +++ b/registry/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'registry.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/registry/registry/__init__.py b/registry/registry/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/registry/registry/asgi.py b/registry/registry/asgi.py new file mode 100644 index 0000000000000000000000000000000000000000..e1d1af068c04c244397d345195498fa0d029ccb0 --- /dev/null +++ b/registry/registry/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for registry project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'registry.settings') + +application = get_asgi_application() diff --git a/registry/registry/settings/base.py b/registry/registry/settings/base.py new file mode 100644 index 0000000000000000000000000000000000000000..6173ee261fe0ea98d32e70e4c32aeeaf32456ed8 --- /dev/null +++ b/registry/registry/settings/base.py @@ -0,0 +1,126 @@ +""" +Django settings for registry project. + +Generated by 'django-admin startproject' using Django 4.0. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +DEBUG = False + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env.str("SECRET_KEY") + +ALLOWED_HOSTS = [] + +STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + + "shared", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "registry.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "registry.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/registry/registry/settings/production.py b/registry/registry/settings/production.py new file mode 100644 index 0000000000000000000000000000000000000000..d8e79df9e8af96e1f8610c856e60ada1189c5c34 --- /dev/null +++ b/registry/registry/settings/production.py @@ -0,0 +1 @@ +from .base import * diff --git a/registry/registry/urls.py b/registry/registry/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..13c37f72e62a07f75bfabf6da7f9aba104c04723 --- /dev/null +++ b/registry/registry/urls.py @@ -0,0 +1,21 @@ +"""registry URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/registry/registry/wsgi.py b/registry/registry/wsgi.py new file mode 100644 index 0000000000000000000000000000000000000000..06fb2293640440935875f76745d3cc20fed4487e --- /dev/null +++ b/registry/registry/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for registry project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'registry.settings') + +application = get_wsgi_application() diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000000000000000000000000000000000000..51af94fe4596c5d69ea0b778a705e673e3e31f30 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,11 @@ +django==4.1.4 +django-database-url==1.0.3 +psycopg2-binary==2.9.5 +django-webpack-loader==1.8.0 +nodeenv==1.7.0 +pirates==0.6.0 +django-markdownx==4.0.0b1 +django-environ==0.9.0 +django-http-exceptions==1.4.0 +django-guardian==2.4.0 +django-countries==7.5.1 diff --git a/requirements/production.txt b/requirements/production.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/shared/admin.py b/shared/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/shared/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/shared/apps.py b/shared/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..87efe0081f84bbf7e0a233e36ae7cdb836755f9e --- /dev/null +++ b/shared/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SharedConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'shared' diff --git a/shared/migrations/__init__.py b/shared/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/shared/models.py b/shared/models.py new file mode 100644 index 0000000000000000000000000000000000000000..8349f55a6685a465eff990b4c37309dd67b6bb08 --- /dev/null +++ b/shared/models.py @@ -0,0 +1,388 @@ +from django.conf import settings +from django.db import models + +from django_countries.fields import CountryField +from markdownx.models import MarkdownxField +from pirates import models as pirates_models + +# Create your models here. + + +class ContractExternalSigner(models.Model): + name = models.CharField( + max_length=256, + verbose_name="Jméno", + ) + + + is_legal_entity = models.BooleanField( + verbose_name="Je právnická osoba", + ) + + + address_street_with_number = models.CharField( + max_length=256, + verbose_name="Ulice, č.p.", + ) # WARNING: Legal entity status dependent! + + address_district = models.CharField( + max_length=256, + verbose_name="Obec", + ) + + address_zip = models.CharField( + max_length=16, + verbose_name="PSČ", + ) # WARNING: Legal entity status dependent! + + address_country = CountryField( + verbose_name="Země", + ) + + + ico_number = models.CharField( + max_length=16, + blank=True, + null=True, + verbose_name="IČO", + ) # WARNING: Legal entity status dependent! + + date_of_birth = models.DateField( + blank=True, + null=True, + verbose_name="Datum narození", + ) # WARNING: Legal entity status dependent! + + + representative_name = models.CharField( + max_length=256, + blank=True, + null=True, + verbose_name="Zástupce", + ) + + representative_role = models.CharField( + max_length=256, + blank=True, + null=True, + verbose_name="Funkce zástupce", + ) + + + department = models.CharField( + max_length=128, + blank=True, + null=True, + verbose_name="Organizační složka", + ) + + + class Meta: + verbose_name = "Druhá smluvní strana" + verbose_name_plural = "Druhé smluvní strany" + + +class ContractLocalSigner(models.Model): + name = models.CharField( + max_length=256, + verbose_name="Jméno", + ) + + + address_street_with_number = models.CharField( + max_length=256, + verbose_name="Ulice, č.p.", + ) + + address_district = models.CharField( + max_length=256, + verbose_name="Obec", + ) + + address_zip = models.CharField( + max_length=16, + verbose_name="PSČ", + ) + + address_country = CountryField( + verbose_name="Země", + ) + + + ico_number = models.CharField( + max_length=16, + blank=True, + null=True, + verbose_name="IČO", + ) + + + # TODO: Input validation + color = models.CharField( + max_length=6, # e.g. "ffffff" + verbose_name="Barva", + ) + + + class Meta: + verbose_name = "Naše smluvní strana" + verbose_name_plural = "Naše smlouvní strany" + + +class ContractSubtypes(models.Model): + name = models.CharField( + max_length=32, + verbose_name="Jméno", + ) + + + class Meta: + verbose_name = "Podtyp smlouvy" + verbose_name_plural = "Podtypy smlouvy" + + +class ContractIssue(models.Model): + name = models.CharField( + max_length=32, + verbose_name="Jméno", + ) + + + class Meta: + verbose_name = "Problém se smlouvou" + verbose_name_plural = "Problémy se smlouvou" + + +class ContractFilingArea(models.Model): + name = models.CharField( + max_length=32, + verbose_name="Jméno", + ) + + person_responsible = models.CharField( + max_length=256, + verbose_name="Odpovědná osoba", + ) + + + class Meta: + verbose_name = "Spisovna" + verbose_name_plural = "Spisovny" + + +class Contract(models.Model): + class ContractTypes(models.TextChoices): + PRIMARY = "Hlavní" + AMENDMENT = "Dodatek" + FRAMEWORK_ORDER = "Objednávka u rámcové smlouvy" + + type_ = models.CharField( + max_length=len(ContractTypes.FRAMEWORK_ORDER), + default=ContractTypes.PRIMARY, + verbose_name="Typ", + ) + + subtype = models.ForeignKey( + ContractSubtype, + on_delete=models.CASCADE, + verbose_name="Podtyp", + ) + + + contains_nda = models.BooleanField( + default=False, + verbose_name="Obsahuje NDA", + ) + + is_anonymized = models.BooleanField( + default=False, + verbose_name="Je anonymizovaná", + ) # WARNING: Seems to only be used for amendments + + + external_signer = models.ForeignKey(ContractExternalSigner) + + # NOTE: Should we allow these to be null, if a contract is logged before it is signed? + external_signer_signature_date = models.DateField( + verbose_name="Datum podpisu druhé strany", + ) + + local_signer = models.ForeignKey(ContractLocalSigner) + + local_signer_signature_date = models.DateField( + verbose_name="Datum podpisu naší strany", + ) + + all_parties_sign_date = models.DateField( + verbose_name="Datum podpisu všech stran", + ) # WARNING: Exclude in admin, autofill + + + valid_start_date = models.DateField( + verbose_name="Začátek účinnosti" + ) + valid_end_date = models.DateField( + verbose_name="Začátek platnosti", + ) + + + uploaded_by = models.ForeignKey( + pirates_models.User, + on_delete=models.CASCADE, + verbose_name="Nahráno uživatelem", + ) + + + class LegalStates(models.TextChoices): + VALID = "Platná" + EFFECTIVE = "Účinná" + NOT_EFFECTIVE = "Neúčinná" + INVALID = "Neplatná" + + class PublicStates(models.TextChoices): + WAITING = "Nová" + YES = "Zveřejněná" + NO = "Neveřejná" + + class PaperFormStates(models.TextChoices): + SENT = "Odeslaná" + STORED = "Uložená" + TO_SHRED = "Ke skartaci" + SHREDDED = "Skartovaná" + + legal_state = models.CharField( + max_length=len(LegalStates.NOT_EFFECTIVE), # longest choice + choices=LegalStates, + verbose_name="Stav právního ujednání", + ) + + public_state = models.CharField( + max_length=len(PublicStates.YES), # longest choice + choices=PublicStates, + verbose_name="Veřejnost smlouvy", + ) + + paper_form_state = models.CharField( + max_length=len(PaperFormStates.TO_SHRED), # longest choice + choices=PaperFormStates, + verbose_name="Stav papírové formy", + ) + + + public_status_set_by = models.ForeignKey( + pirates_models.User, + on_delete=models.CASCADE, + verbose_name="Zveřejněno / nezveřejněno uživatelem", + ) + + + publishing_rejection_comment = models.CharField( + max_length=65536, + blank=True, + null=True, + verbose_name="Důvod nezveřejnění", + ) + + + tender_url = models.URLField( + max_length=256, + blank=True, + null=True, + verbose_name="Odkaz na výběrové řízení", + ) + + + identifier = models.CharField( + max_length=128, + verbose_name="Identifikační číslo", + ) + + + issues = modles.ManyToManyField(ContractIssue) + + + summary = models.CharField( + max_length=65536, + blank=True, + null=True, + verbose_name="Rekapitulace", + ) + + + contract_file = models.FileField( + verbose_name="Smlouva (PDF)", + ) + + primary_contract = models.ForeignKey( + Contract, + blank=True, + null=True, + verbose_name="Hlavní smlouva", + ) # WARNING: Dependent on the type! + + + expected_cost_total = models.IntegerField( + verbose_name="Očekáváná celková cena" + ) + + expected_cost_year = models.IntegerField( + verbose_name="Očekáváná cena za rok" + ) + + expected_cost_month = models.IntegerField( + verbose_name="Očekáváná cena za měsíc" + ) + + expected_cost_hour = models.IntegerField( + verbose_name="Očekáváná cena za hodinu" + ) + + + intent_url = models.URLField( + max_length=256, + blank=True, + null=True, + verbose_name="Odkaz na záměr", + ) + + agreement_url = models.URLField( + max_length=256, + blank=True, + null=True, + verbose_name="Odkaz na schválení", + ) # WARNING: Dependent on the type! + + + filing_area = models.ForeignKey( + ContractFilingArea, + blank=True, + null=True, + ) # WARNING: Dependent on the type! + + + class Meta: + verbose_name = "Smlouva" + verbose_name_plural = "Smlouvy" + + +class ContractNote(models.Model): + contract = models.ForeignKey(Contract) + + + author = models.ForeignKey( + pirates_models.User, + verbose_name="Autor", + ) + + created_date = models.DateTimeField( + verbose_name="Datum vytvoření", + ) + + content = models.MarkdownxField( + verbose_name="Obsah", + ) + + + class Meta: + verbose_name = "Poznámka ke smlouvě" + verbose_name_plural = "Poznámky ke smlouvě" diff --git a/shared/tests.py b/shared/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/shared/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/shared/views.py b/shared/views.py new file mode 100644 index 0000000000000000000000000000000000000000..91ea44a218fbd2f408430959283f0419c921093e --- /dev/null +++ b/shared/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/static_src/base.css b/static_src/base.css new file mode 100644 index 0000000000000000000000000000000000000000..44f1cb603a7646e785005d3627740563ef4fb9e4 --- /dev/null +++ b/static_src/base.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + + +@layer base { + html { + font-family: "Roboto Condensed", system-ui, sans-serif; + } +} + + +@layer typography { + .font-bebas { + font-family: "Bebas Neue"; + } +} diff --git a/static_src/base.js b/static_src/base.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..882ff8fc6f21218aac61604077d8e0cdd47ff91e --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,18 @@ +const defaultTheme = require("tailwindcss/defaultTheme"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "*/templates/*/*.html", + "*/templates/*/*/*.html", + ], + theme: { + extend: { + fontFamily: { + "bebas": ["Bebas Neue", defaultTheme.fontFamily.sans], + "sans": ["Roboto Condensed", defaultTheme.fontFamily.sans], + }, + }, + }, + plugins: [], +} diff --git a/webpack-stats.json b/webpack-stats.json new file mode 100644 index 0000000000000000000000000000000000000000..3e296a93a0be9d13148309d286fffa4781fc9b02 --- /dev/null +++ b/webpack-stats.json @@ -0,0 +1,30 @@ +{ + "status": "done", + "assets": { + "base-1150289fada5f20f5bd0.js": { + "name": "base-1150289fada5f20f5bd0.js", + "path": "/home/user/Projects/contract-registry/shared/static/shared/base-1150289fada5f20f5bd0.js" + }, + "runtime-1150289fada5f20f5bd0.js": { + "name": "runtime-1150289fada5f20f5bd0.js", + "path": "/home/user/Projects/contract-registry/shared/static/shared/runtime-1150289fada5f20f5bd0.js" + }, + "shared-1150289fada5f20f5bd0.js": { + "name": "shared-1150289fada5f20f5bd0.js", + "path": "/home/user/Projects/contract-registry/shared/static/shared/shared-1150289fada5f20f5bd0.js" + }, + "shared-1150289fada5f20f5bd0.js.LICENSE.txt": { + "name": "shared-1150289fada5f20f5bd0.js.LICENSE.txt", + "path": "/home/user/Projects/contract-registry/shared/static/shared/shared-1150289fada5f20f5bd0.js.LICENSE.txt" + } + }, + "chunks": { + "base": [ + "base-1150289fada5f20f5bd0.js" + ], + "shared": [ + "runtime-1150289fada5f20f5bd0.js", + "shared-1150289fada5f20f5bd0.js" + ] + } +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000000000000000000000000000000000000..5fd34684fb877b918b136bc58fd1e1fe949db07d --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,32 @@ +const path = require('path'); +const BundleTracker = require('webpack-bundle-tracker'); + +module.exports = { + mode: "production", + context: __dirname, + entry: { + base: { + import: path.resolve("static_src", "base.js"), + dependOn: "shared", + }, + shared: ["jquery"], + }, + output: { + path: path.resolve(__dirname, "shared", "static", "shared"), + filename: "[name]-[fullhash].js", + }, + module: { + rules: [ + { + test: /\.css$/i, + use: ["style-loader", "css-loader"], + }, + ], + }, + optimization: { + runtimeChunk: "single", + }, + plugins: [ + new BundleTracker({filename: './webpack-stats.json'}) + ], +};