diff --git a/.gitignore b/.gitignore index 0efabcf..f7e515a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ app/static/cache/* app/cypress/screenshots/* .ruff_cache/ app/node_modules/ +app/static/vendor/ hadolint-results.sarif build/ *.egg-info/ diff --git a/Dockerfile b/Dockerfile index 2c6aee4..3b47662 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ FLASK_HOST=0.0.0.0 +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm && rm -rf /var/lib/apt/lists/* + WORKDIR /tmp/build COPY pyproject.toml README.md main.py ./ @@ -12,5 +15,6 @@ RUN python -m pip install --no-cache-dir . WORKDIR /app COPY app/ . +RUN npm install --prefix /app CMD ["python", "app.py"] diff --git a/app/package.json b/app/package.json index ab301ee..a41b3de 100644 --- a/app/package.json +++ b/app/package.json @@ -1,5 +1,16 @@ { + "dependencies": { + "@fortawesome/fontawesome-free": "^6.7.2", + "bootstrap": "5.2.2", + "bootstrap-icons": "1.9.1", + "jquery": "3.6.0", + "marked": "^4.3.0" + }, "devDependencies": { "cypress": "^14.5.1" + }, + "scripts": { + "build": "node scripts/copy-vendor.js", + "postinstall": "node scripts/copy-vendor.js" } } diff --git a/app/scripts/copy-vendor.js b/app/scripts/copy-vendor.js new file mode 100644 index 0000000..dcf516d --- /dev/null +++ b/app/scripts/copy-vendor.js @@ -0,0 +1,71 @@ +'use strict'; +/** + * Copies third-party browser assets from node_modules into static/vendor/ + * so Flask can serve them without any CDN dependency. + * Runs automatically via the "postinstall" npm hook. + */ +const fs = require('fs'); +const path = require('path'); + +const NM = path.join(__dirname, '..', 'node_modules'); +const VENDOR = path.join(__dirname, '..', 'static', 'vendor'); + +function copyFile(src, dest) { + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); +} + +function copyDir(src, dest) { + fs.mkdirSync(dest, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + const s = path.join(src, entry.name); + const d = path.join(dest, entry.name); + entry.isDirectory() ? copyDir(s, d) : fs.copyFileSync(s, d); + } +} + +// Bootstrap CSS + JS bundle +copyFile( + path.join(NM, 'bootstrap', 'dist', 'css', 'bootstrap.min.css'), + path.join(VENDOR, 'bootstrap', 'css', 'bootstrap.min.css') +); +copyFile( + path.join(NM, 'bootstrap', 'dist', 'js', 'bootstrap.bundle.min.js'), + path.join(VENDOR, 'bootstrap', 'js', 'bootstrap.bundle.min.js') +); + +// Bootstrap Icons CSS + embedded fonts +copyFile( + path.join(NM, 'bootstrap-icons', 'font', 'bootstrap-icons.css'), + path.join(VENDOR, 'bootstrap-icons', 'font', 'bootstrap-icons.css') +); +copyDir( + path.join(NM, 'bootstrap-icons', 'font', 'fonts'), + path.join(VENDOR, 'bootstrap-icons', 'font', 'fonts') +); + +// Font Awesome Free CSS + webfonts +copyFile( + path.join(NM, '@fortawesome', 'fontawesome-free', 'css', 'all.min.css'), + path.join(VENDOR, 'fontawesome', 'css', 'all.min.css') +); +copyDir( + path.join(NM, '@fortawesome', 'fontawesome-free', 'webfonts'), + path.join(VENDOR, 'fontawesome', 'webfonts') +); + +// marked – browser UMD build (path varies by version) +const markedCandidates = [ + path.join(NM, 'marked', 'marked.min.js'), // v4.x + path.join(NM, 'marked', 'lib', 'marked.umd.min.js'), // v5.x + path.join(NM, 'marked', 'dist', 'marked.min.js'), // v9+ +]; +const markedSrc = markedCandidates.find(p => fs.existsSync(p)); +if (!markedSrc) throw new Error('marked: no browser UMD build found in node_modules'); +copyFile(markedSrc, path.join(VENDOR, 'marked', 'marked.min.js')); + +// jQuery +copyFile( + path.join(NM, 'jquery', 'dist', 'jquery.min.js'), + path.join(VENDOR, 'jquery', 'jquery.min.js') +); diff --git a/app/templates/moduls/base.html.j2 b/app/templates/moduls/base.html.j2 index 8a57fc5..fdf00e4 100644 --- a/app/templates/moduls/base.html.j2 +++ b/app/templates/moduls/base.html.j2 @@ -9,22 +9,19 @@ href="{% if platform.favicon.cache %}{{ url_for('static', filename=platform.favicon.cache) }}{% endif %}" > - + - + - + - + - + - +