diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e431fd5f7f..643ef13fd0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -50,6 +50,7 @@ jobs:
permissions:
actions: read
security-events: write
+ pull-requests: write
strategy:
fail-fast: false
matrix:
@@ -84,6 +85,60 @@ jobs:
gradle-version: 9.3.1
cache-disabled: true
+ - name: Check Java formatting (Spotless)
+ if: matrix.jdk-version == 25 && matrix.spring-security == false
+ id: spotless-check
+ run: ./gradlew spotlessCheck
+ continue-on-error: true
+ env:
+ MAVEN_USER: ${{ secrets.MAVEN_USER }}
+ MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
+ MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
+
+ - name: Comment on Java formatting failure
+ if: steps.spotless-check.outcome == 'failure'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: |
+ const marker = '';
+ const body = [
+ marker,
+ '### Java Formatting Check Failed',
+ '',
+ 'Your code has formatting issues. Run the following command to fix them:',
+ '',
+ '```bash',
+ './gradlew spotlessApply',
+ '```',
+ '',
+ 'Then commit and push the changes.',
+ ].join('\n');
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+ const existing = comments.find(c => c.body.includes(marker));
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body,
+ });
+ }
+
+ - name: Fail if Java formatting issues found
+ if: steps.spotless-check.outcome == 'failure'
+ run: exit 1
+
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
run: ./gradlew build -PnoSpotless
env:
@@ -187,6 +242,9 @@ jobs:
if: needs.files-changed.outputs.frontend == 'true'
needs: files-changed
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
@@ -202,6 +260,52 @@ jobs:
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
run: cd frontend && npm ci
+ - name: Check TypeScript formatting (Prettier)
+ id: prettier-check
+ run: cd frontend && npm run format:check
+ continue-on-error: true
+ - name: Comment on TypeScript formatting failure
+ if: steps.prettier-check.outcome == 'failure'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: |
+ const marker = '';
+ const body = [
+ marker,
+ '### TypeScript Formatting Check Failed',
+ '',
+ 'Your code has formatting issues. Run the following command to fix them:',
+ '',
+ '```bash',
+ 'cd frontend && npm run fix',
+ '```',
+ '',
+ 'Then commit and push the changes.',
+ ].join('\n');
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+ const existing = comments.find(c => c.body.includes(marker));
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body,
+ });
+ }
+ - name: Fail if TypeScript formatting issues found
+ if: steps.prettier-check.outcome == 'failure'
+ run: exit 1
- name: Type-check frontend
run: cd frontend && npm run prep && npm run typecheck:all
- name: Lint frontend
diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml
index 764f748d44..9e4d5d574f 100644
--- a/.github/workflows/pre_commit.yml
+++ b/.github/workflows/pre_commit.yml
@@ -2,7 +2,7 @@ name: Pre-commit
on:
workflow_dispatch:
- push:
+ pull_request:
branches:
- main
@@ -16,9 +16,6 @@ jobs:
# Prevents sdist builds ā no tar extraction
PIP_ONLY_BINARY: ":all:"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
- permissions:
- contents: write
- pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
@@ -31,13 +28,6 @@ jobs:
fetch-depth: 0
persist-credentials: false
- - name: Setup GitHub App Bot
- id: setup-bot
- uses: ./.github/actions/setup-bot
- with:
- app-id: ${{ secrets.GH_APP_ID }}
- private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
-
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
@@ -57,47 +47,4 @@ jobs:
pre-commit run gitleaks --all-files -c .pre-commit-config.yaml
pre-commit run end-of-file-fixer --all-files -c .pre-commit-config.yaml
pre-commit run trailing-whitespace --all-files -c .pre-commit-config.yaml
- continue-on-error: true
-
- - name: Set up JDK 25
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
- with:
- java-version: "25"
- distribution: "temurin"
-
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
- with:
- gradle-version: 9.3.1
-
- - name: Build with Gradle
- run: ./gradlew build
- env:
- MAVEN_USER: ${{ secrets.MAVEN_USER }}
- MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
- MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
-
- - name: git add
- run: |
- git add .
- git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
-
- - name: Create Pull Request
- if: env.CHANGES_DETECTED == 'true'
- uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
- with:
- token: ${{ steps.setup-bot.outputs.token }}
- commit-message: ":file_folder: pre-commit"
- committer: ${{ steps.setup-bot.outputs.committer }}
- author: ${{ steps.setup-bot.outputs.committer }}
- signoff: true
- branch: pre-commit
- title: "š¤ format everything with pre-commit by ${{ steps.setup-bot.outputs.app-slug }}"
- body: |
- Auto-generated by [create-pull-request][1] with **${{ steps.setup-bot.outputs.app-slug }}**
-
- [1]: https://github.com/peter-evans/create-pull-request
- draft: false
- delete-branch: true
- labels: github-actions
- sign-commits: true
+ git diff --exit-code
diff --git a/build.gradle b/build.gradle
index 62e92d9484..0721c37baa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -110,11 +110,11 @@ tasks.register('syncAppVersion') {
[new File(sim1Path), new File(sim2Path)].each { f ->
if (f.exists()) {
def content = f.getText('UTF-8')
- def matcher = (content =~ /(appVersion:\s*')([^']*)(')/)
+ def matcher = (content =~ /(appVersion:\s*(['"]))(.*?)(\2)/)
if (!matcher.find()) {
throw new GradleException("Could not locate appVersion in ${f} for synchronization")
}
- def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(3)}")
+ def updatedContent = matcher.replaceFirst("${matcher.group(1)}${appVersionStr}${matcher.group(4)}")
if (content != updatedContent) {
f.write(updatedContent, 'UTF-8')
}
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000000..ad5dab21a1
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,10 @@
+dist/
+node_modules/
+public/vendor/
+public/pdfjs*/
+public/js/thirdParty/
+public/css/cookieconsent.css
+*.min.*
+*.md
+*.wxs
+src/output.css
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
index 9bbe913d60..de728c2775 100644
--- a/frontend/eslint.config.mjs
+++ b/frontend/eslint.config.mjs
@@ -1,62 +1,52 @@
// @ts-check
-import eslint from '@eslint/js';
-import globals from 'globals';
-import { defineConfig } from 'eslint/config';
-import tseslint from 'typescript-eslint';
+import eslint from "@eslint/js";
+import globals from "globals";
+import { defineConfig } from "eslint/config";
+import tseslint from "typescript-eslint";
-const srcGlobs = [
- 'src/**/*.{js,mjs,jsx,ts,tsx}',
-];
-const nodeGlobs = [
- 'scripts/**/*.{js,ts,mjs}',
- '*.config.{js,ts,mjs}',
-];
+const srcGlobs = ["src/**/*.{js,mjs,jsx,ts,tsx}"];
+const nodeGlobs = ["scripts/**/*.{js,ts,mjs}", "*.config.{js,ts,mjs}"];
const baseRestrictedImportPatterns = [
- { regex: '^\\.', message: "Use @app/* imports instead of relative imports." },
- { regex: '^src/', message: "Use @app/* imports instead of absolute src/ imports." },
+ { regex: "^\\.", message: "Use @app/* imports instead of relative imports." },
+ { regex: "^src/", message: "Use @app/* imports instead of absolute src/ imports." },
];
export default defineConfig(
{
// Everything that contains 3rd party code that we don't want to lint
- ignores: [
- 'dist',
- 'node_modules',
- 'public',
- 'src-tauri',
- ],
+ ignores: ["dist", "node_modules", "public", "src-tauri"],
},
eslint.configs.recommended,
tseslint.configs.recommended,
{
rules: {
- 'no-restricted-imports': [
- 'error',
+ "no-restricted-imports": [
+ "error",
{
patterns: baseRestrictedImportPatterns,
},
],
- '@typescript-eslint/no-empty-object-type': [
- 'error',
+ "@typescript-eslint/no-empty-object-type": [
+ "error",
{
// Allow empty extending interfaces because there's no real reason not to, and it makes it obvious where to put extra attributes in the future
- allowInterfaces: 'with-single-extends',
+ allowInterfaces: "with-single-extends",
},
],
- '@typescript-eslint/no-explicit-any': 'off', // Temporarily disabled until codebase conformant
- '@typescript-eslint/no-require-imports': 'off', // Temporarily disabled until codebase conformant
- '@typescript-eslint/no-unused-vars': [
- 'error',
+ "@typescript-eslint/no-explicit-any": "off", // Temporarily disabled until codebase conformant
+ "@typescript-eslint/no-require-imports": "off", // Temporarily disabled until codebase conformant
+ "@typescript-eslint/no-unused-vars": [
+ "error",
{
- 'args': 'all', // All function args must be used (or explicitly ignored)
- 'argsIgnorePattern': '^_', // Allow unused variables beginning with an underscore
- 'caughtErrors': 'all', // Caught errors must be used (or explicitly ignored)
- 'caughtErrorsIgnorePattern': '^_', // Allow unused variables beginning with an underscore
- 'destructuredArrayIgnorePattern': '^_', // Allow unused variables beginning with an underscore
- 'varsIgnorePattern': '^_', // Allow unused variables beginning with an underscore
- 'ignoreRestSiblings': true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky)
+ args: "all", // All function args must be used (or explicitly ignored)
+ argsIgnorePattern: "^_", // Allow unused variables beginning with an underscore
+ caughtErrors: "all", // Caught errors must be used (or explicitly ignored)
+ caughtErrorsIgnorePattern: "^_", // Allow unused variables beginning with an underscore
+ destructuredArrayIgnorePattern: "^_", // Allow unused variables beginning with an underscore
+ varsIgnorePattern: "^_", // Allow unused variables beginning with an underscore
+ ignoreRestSiblings: true, // Allow unused variables when removing attributes from objects (otherwise this requires explicit renaming like `({ x: _x, ...y }) => y`, which is clunky)
},
],
},
@@ -65,15 +55,15 @@ export default defineConfig(
// Use the stub/shadow pattern instead: define a stub in src/core/ and override in src/desktop/.
{
files: srcGlobs,
- ignores: ['src/desktop/**'],
+ ignores: ["src/desktop/**"],
rules: {
- 'no-restricted-imports': [
- 'error',
+ "no-restricted-imports": [
+ "error",
{
patterns: [
...baseRestrictedImportPatterns,
{
- regex: '^@tauri-apps/',
+ regex: "^@tauri-apps/",
message: "Tauri APIs are desktop-only. Review frontend/DeveloperGuide.md for structure advice.",
},
],
@@ -84,9 +74,9 @@ export default defineConfig(
// Folders that have been cleaned up and are now conformant - stricter rules enforced here
{
files: [
- 'src/proprietary/**/*.{js,mjs,jsx,ts,tsx}',
- 'src/saas/**/*.{js,mjs,jsx,ts,tsx}',
- 'src/prototypes/**/*.{js,mjs,jsx,ts,tsx}',
+ "src/proprietary/**/*.{js,mjs,jsx,ts,tsx}",
+ "src/saas/**/*.{js,mjs,jsx,ts,tsx}",
+ "src/prototypes/**/*.{js,mjs,jsx,ts,tsx}",
],
languageOptions: {
parserOptions: {
@@ -95,8 +85,8 @@ export default defineConfig(
},
},
rules: {
- '@typescript-eslint/no-explicit-any': 'error',
- '@typescript-eslint/no-unnecessary-type-assertion': 'error',
+ "@typescript-eslint/no-explicit-any": "error",
+ "@typescript-eslint/no-unnecessary-type-assertion": "error",
},
},
// Config for browser scripts
@@ -105,8 +95,8 @@ export default defineConfig(
languageOptions: {
globals: {
...globals.browser,
- }
- }
+ },
+ },
},
// Config for node scripts
{
@@ -114,7 +104,7 @@ export default defineConfig(
languageOptions: {
globals: {
...globals.node,
- }
- }
+ },
+ },
},
);
diff --git a/frontend/index.html b/frontend/index.html
index c790b8d1ed..465841285d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,4 +1,4 @@
-
+
@@ -6,10 +6,7 @@
-
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index dbf6bbd484..d6a59cbed7 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -114,6 +114,7 @@
"postcss-cli": "^11.0.1",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
+ "prettier": "^3.8.1",
"puppeteer": "^24.25.0",
"tsx": "^4.21.0",
"typescript": "^5.9.2",
@@ -11308,6 +11309,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 93051e2dc7..167fd7181e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -89,8 +89,12 @@
"dev:saas": "npm run prep:saas && vite --mode saas",
"dev:desktop": "npm run prep:desktop && vite --mode desktop",
"dev:prototypes": "npm run prep && vite --mode prototypes",
+ "fix": "npm run format && npm run lint:fix",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
"lint": "npm run lint:eslint && npm run lint:cycles",
"lint:eslint": "eslint --max-warnings=0",
+ "lint:fix": "eslint --fix",
"lint:cycles": "dpdm src --circular --no-warning --no-tree --exit-code circular:1",
"build": "npm run prep && vite build",
"build:core": "npm run prep && vite build --mode core",
@@ -176,6 +180,7 @@
"postcss-cli": "^11.0.1",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
+ "prettier": "^3.8.1",
"puppeteer": "^24.25.0",
"tsx": "^4.21.0",
"typescript": "^5.9.2",
diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts
index 14eb25f85c..deb6a63977 100644
--- a/frontend/playwright.config.ts
+++ b/frontend/playwright.config.ts
@@ -1,11 +1,11 @@
-import { defineConfig, devices } from '@playwright/test';
+import { defineConfig, devices } from "@playwright/test";
/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
- testDir: './src/core/tests',
- testMatch: '**/*.spec.ts',
+ testDir: "./src/core/tests",
+ testMatch: "**/*.spec.ts",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -15,34 +15,34 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
- reporter: 'html',
+ reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
- baseURL: 'http://localhost:5173',
+ baseURL: "http://localhost:5173",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
- trace: 'on-first-retry',
+ trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
- name: 'chromium',
- use: {
- ...devices['Desktop Chrome'],
- viewport: { width: 1920, height: 1080 }
+ name: "chromium",
+ use: {
+ ...devices["Desktop Chrome"],
+ viewport: { width: 1920, height: 1080 },
},
},
{
- name: 'firefox',
- use: { ...devices['Desktop Firefox'] },
+ name: "firefox",
+ use: { ...devices["Desktop Firefox"] },
},
{
- name: 'webkit',
- use: { ...devices['Desktop Safari'] },
+ name: "webkit",
+ use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
@@ -68,8 +68,8 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
- command: 'npm run dev',
- url: 'http://localhost:5173',
+ command: "npm run dev",
+ url: "http://localhost:5173",
reuseExistingServer: !process.env.CI,
},
-});
\ No newline at end of file
+});
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
index 57e730c99a..7b8895cce8 100644
--- a/frontend/postcss.config.js
+++ b/frontend/postcss.config.js
@@ -1,6 +1,3 @@
module.exports = {
- plugins: [
- require('@tailwindcss/postcss'),
- require('autoprefixer'),
- ],
+ plugins: [require("@tailwindcss/postcss"), require("autoprefixer")],
};
diff --git a/frontend/public/css/cookieconsentCustomisation.css b/frontend/public/css/cookieconsentCustomisation.css
index ec360c20be..fd1a8ff355 100644
--- a/frontend/public/css/cookieconsentCustomisation.css
+++ b/frontend/public/css/cookieconsentCustomisation.css
@@ -1,206 +1,205 @@
/* Light theme variables */
:root {
- --cc-bg: #ffffff;
- --cc-primary-color: #1c1c1c;
- --cc-secondary-color: #666666;
+ --cc-bg: #ffffff;
+ --cc-primary-color: #1c1c1c;
+ --cc-secondary-color: #666666;
- --cc-btn-primary-bg: #007BFF;
- --cc-btn-primary-color: #ffffff;
- --cc-btn-primary-border-color: #007BFF;
- --cc-btn-primary-hover-bg: #0056b3;
- --cc-btn-primary-hover-color: #ffffff;
- --cc-btn-primary-hover-border-color: #0056b3;
+ --cc-btn-primary-bg: #007bff;
+ --cc-btn-primary-color: #ffffff;
+ --cc-btn-primary-border-color: #007bff;
+ --cc-btn-primary-hover-bg: #0056b3;
+ --cc-btn-primary-hover-color: #ffffff;
+ --cc-btn-primary-hover-border-color: #0056b3;
- --cc-btn-secondary-bg: #f1f3f4;
- --cc-btn-secondary-color: #1c1c1c;
- --cc-btn-secondary-border-color: #f1f3f4;
- --cc-btn-secondary-hover-bg: #007BFF;
- --cc-btn-secondary-hover-color: #ffffff;
- --cc-btn-secondary-hover-border-color: #007BFF;
+ --cc-btn-secondary-bg: #f1f3f4;
+ --cc-btn-secondary-color: #1c1c1c;
+ --cc-btn-secondary-border-color: #f1f3f4;
+ --cc-btn-secondary-hover-bg: #007bff;
+ --cc-btn-secondary-hover-color: #ffffff;
+ --cc-btn-secondary-hover-border-color: #007bff;
- --cc-separator-border-color: #e0e0e0;
+ --cc-separator-border-color: #e0e0e0;
- --cc-toggle-on-bg: #007BFF;
- --cc-toggle-off-bg: #667481;
- --cc-toggle-on-knob-bg: #ffffff;
- --cc-toggle-off-knob-bg: #ffffff;
+ --cc-toggle-on-bg: #007bff;
+ --cc-toggle-off-bg: #667481;
+ --cc-toggle-on-knob-bg: #ffffff;
+ --cc-toggle-off-knob-bg: #ffffff;
- --cc-toggle-enabled-icon-color: #ffffff;
- --cc-toggle-disabled-icon-color: #ffffff;
+ --cc-toggle-enabled-icon-color: #ffffff;
+ --cc-toggle-disabled-icon-color: #ffffff;
- --cc-toggle-readonly-bg: #f1f3f4;
- --cc-toggle-readonly-knob-bg: #79747E;
- --cc-toggle-readonly-knob-icon-color: #f1f3f4;
+ --cc-toggle-readonly-bg: #f1f3f4;
+ --cc-toggle-readonly-knob-bg: #79747e;
+ --cc-toggle-readonly-knob-icon-color: #f1f3f4;
- --cc-section-category-border: #e0e0e0;
+ --cc-section-category-border: #e0e0e0;
- --cc-cookie-category-block-bg: #f1f3f4;
- --cc-cookie-category-block-border: #f1f3f4;
- --cc-cookie-category-block-hover-bg: #e9eff4;
- --cc-cookie-category-block-hover-border: #e9eff4;
-
- --cc-cookie-category-expanded-block-bg: #f1f3f4;
- --cc-cookie-category-expanded-block-hover-bg: #e9eff4;
+ --cc-cookie-category-block-bg: #f1f3f4;
+ --cc-cookie-category-block-border: #f1f3f4;
+ --cc-cookie-category-block-hover-bg: #e9eff4;
+ --cc-cookie-category-block-hover-border: #e9eff4;
- --cc-footer-bg: #ffffff;
- --cc-footer-color: #1c1c1c;
- --cc-footer-border-color: #ffffff;
+ --cc-cookie-category-expanded-block-bg: #f1f3f4;
+ --cc-cookie-category-expanded-block-hover-bg: #e9eff4;
+
+ --cc-footer-bg: #ffffff;
+ --cc-footer-color: #1c1c1c;
+ --cc-footer-border-color: #ffffff;
}
/* Dark theme variables */
-.cc--darkmode{
- --cc-bg: #2d2d2d;
- --cc-primary-color: #e5e5e5;
- --cc-secondary-color: #b0b0b0;
+.cc--darkmode {
+ --cc-bg: #2d2d2d;
+ --cc-primary-color: #e5e5e5;
+ --cc-secondary-color: #b0b0b0;
- --cc-btn-primary-bg: #4dabf7;
- --cc-btn-primary-color: #ffffff;
- --cc-btn-primary-border-color: #4dabf7;
- --cc-btn-primary-hover-bg: #3d3d3d;
- --cc-btn-primary-hover-color: #ffffff;
- --cc-btn-primary-hover-border-color: #3d3d3d;
+ --cc-btn-primary-bg: #4dabf7;
+ --cc-btn-primary-color: #ffffff;
+ --cc-btn-primary-border-color: #4dabf7;
+ --cc-btn-primary-hover-bg: #3d3d3d;
+ --cc-btn-primary-hover-color: #ffffff;
+ --cc-btn-primary-hover-border-color: #3d3d3d;
- --cc-btn-secondary-bg: #3d3d3d;
- --cc-btn-secondary-color: #ffffff;
- --cc-btn-secondary-border-color: #3d3d3d;
- --cc-btn-secondary-hover-bg: #4dabf7;
- --cc-btn-secondary-hover-color: #ffffff;
- --cc-btn-secondary-hover-border-color: #4dabf7;
+ --cc-btn-secondary-bg: #3d3d3d;
+ --cc-btn-secondary-color: #ffffff;
+ --cc-btn-secondary-border-color: #3d3d3d;
+ --cc-btn-secondary-hover-bg: #4dabf7;
+ --cc-btn-secondary-hover-color: #ffffff;
+ --cc-btn-secondary-hover-border-color: #4dabf7;
- --cc-separator-border-color: #555555;
+ --cc-separator-border-color: #555555;
- --cc-toggle-on-bg: #4dabf7;
- --cc-toggle-off-bg: #667481;
- --cc-toggle-on-knob-bg: #2d2d2d;
- --cc-toggle-off-knob-bg: #2d2d2d;
+ --cc-toggle-on-bg: #4dabf7;
+ --cc-toggle-off-bg: #667481;
+ --cc-toggle-on-knob-bg: #2d2d2d;
+ --cc-toggle-off-knob-bg: #2d2d2d;
- --cc-toggle-enabled-icon-color: #2d2d2d;
- --cc-toggle-disabled-icon-color: #2d2d2d;
+ --cc-toggle-enabled-icon-color: #2d2d2d;
+ --cc-toggle-disabled-icon-color: #2d2d2d;
- --cc-toggle-readonly-bg: #555555;
- --cc-toggle-readonly-knob-bg: #8e8e8e;
- --cc-toggle-readonly-knob-icon-color: #555555;
+ --cc-toggle-readonly-bg: #555555;
+ --cc-toggle-readonly-knob-bg: #8e8e8e;
+ --cc-toggle-readonly-knob-icon-color: #555555;
- --cc-section-category-border: #555555;
+ --cc-section-category-border: #555555;
- --cc-cookie-category-block-bg: #3d3d3d;
- --cc-cookie-category-block-border: #3d3d3d;
- --cc-cookie-category-block-hover-bg: #4d4d4d;
- --cc-cookie-category-block-hover-border: #4d4d4d;
-
- --cc-cookie-category-expanded-block-bg: #3d3d3d;
- --cc-cookie-category-expanded-block-hover-bg: #4d4d4d;
+ --cc-cookie-category-block-bg: #3d3d3d;
+ --cc-cookie-category-block-border: #3d3d3d;
+ --cc-cookie-category-block-hover-bg: #4d4d4d;
+ --cc-cookie-category-block-hover-border: #4d4d4d;
- --cc-footer-bg: #2d2d2d;
- --cc-footer-color: #e5e5e5;
- --cc-footer-border-color: #2d2d2d;
+ --cc-cookie-category-expanded-block-bg: #3d3d3d;
+ --cc-cookie-category-expanded-block-hover-bg: #4d4d4d;
+
+ --cc-footer-bg: #2d2d2d;
+ --cc-footer-color: #e5e5e5;
+ --cc-footer-border-color: #2d2d2d;
}
-.cm__body{
- max-width: 90% !important;
- flex-direction: row !important;
- align-items: center !important;
-
+.cm__body {
+ max-width: 90% !important;
+ flex-direction: row !important;
+ align-items: center !important;
}
-.cm__desc{
- max-width: 70rem !important;
+.cm__desc {
+ max-width: 70rem !important;
}
-.cm__btns{
- flex-direction: row-reverse !important;
- gap:10px !important;
- padding-top: 3.4rem !important;
+.cm__btns {
+ flex-direction: row-reverse !important;
+ gap: 10px !important;
+ padding-top: 3.4rem !important;
}
@media only screen and (max-width: 1400px) {
- .cm__body{
- max-width: 90% !important;
- flex-direction: column !important;
- align-items: normal !important;
- }
+ .cm__body {
+ max-width: 90% !important;
+ flex-direction: column !important;
+ align-items: normal !important;
+ }
- .cm__btns{
- padding-top: 1rem !important;
- }
+ .cm__btns {
+ padding-top: 1rem !important;
+ }
}
/* Toggle visibility fixes */
#cc-main .section__toggle {
- opacity: 0 !important; /* Keep invisible but functional */
+ opacity: 0 !important; /* Keep invisible but functional */
}
#cc-main .toggle__icon {
- display: flex !important;
- align-items: center !important;
- justify-content: flex-start !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: flex-start !important;
}
#cc-main .toggle__icon-circle {
- display: block !important;
- position: absolute !important;
- transition: transform 0.25s ease !important;
+ display: block !important;
+ position: absolute !important;
+ transition: transform 0.25s ease !important;
}
#cc-main .toggle__icon-on,
#cc-main .toggle__icon-off {
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- position: absolute !important;
- width: 100% !important;
- height: 100% !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ position: absolute !important;
+ width: 100% !important;
+ height: 100% !important;
}
/* Ensure toggles are visible in both themes */
#cc-main .toggle__icon {
- background: var(--cc-toggle-off-bg) !important;
- border: 1px solid var(--cc-toggle-off-bg) !important;
+ background: var(--cc-toggle-off-bg) !important;
+ border: 1px solid var(--cc-toggle-off-bg) !important;
}
#cc-main .section__toggle:checked ~ .toggle__icon {
- background: var(--cc-toggle-on-bg) !important;
- border: 1px solid var(--cc-toggle-on-bg) !important;
+ background: var(--cc-toggle-on-bg) !important;
+ border: 1px solid var(--cc-toggle-on-bg) !important;
}
/* Ensure toggle text is visible */
#cc-main .pm__section-title {
- color: var(--cc-primary-color) !important;
+ color: var(--cc-primary-color) !important;
}
#cc-main .pm__section-desc {
- color: var(--cc-secondary-color) !important;
+ color: var(--cc-secondary-color) !important;
}
/* Make sure the modal has proper contrast */
#cc-main .pm {
- background: var(--cc-bg) !important;
- color: var(--cc-primary-color) !important;
+ background: var(--cc-bg) !important;
+ color: var(--cc-primary-color) !important;
}
/* Lower z-index so cookie banner appears behind onboarding modals */
#cc-main {
- z-index: 100 !important;
+ z-index: 100 !important;
}
/* Ensure consent modal text is visible in both themes */
#cc-main .cm {
- background: var(--cc-bg) !important;
- color: var(--cc-primary-color) !important;
+ background: var(--cc-bg) !important;
+ color: var(--cc-primary-color) !important;
}
#cc-main .cm__title {
- color: var(--cc-primary-color) !important;
+ color: var(--cc-primary-color) !important;
}
#cc-main .cm__desc {
- color: var(--cc-primary-color) !important;
+ color: var(--cc-primary-color) !important;
}
#cc-main .cm__footer {
- color: var(--cc-primary-color) !important;
+ color: var(--cc-primary-color) !important;
}
#cc-main .cm__footer-links a,
#cc-main .cm__link {
- color: var(--cc-primary-color) !important;
-}
\ No newline at end of file
+ color: var(--cc-primary-color) !important;
+}
diff --git a/frontend/public/manifest-classic.json b/frontend/public/manifest-classic.json
index 9b47da7d05..d6e81e7ddf 100644
--- a/frontend/public/manifest-classic.json
+++ b/frontend/public/manifest-classic.json
@@ -23,4 +23,3 @@
"theme_color": "#000000",
"background_color": "#ffffff"
}
-
diff --git a/frontend/scripts/build-provisioner.mjs b/frontend/scripts/build-provisioner.mjs
index 2f974a195f..52240b766e 100644
--- a/frontend/scripts/build-provisioner.mjs
+++ b/frontend/scripts/build-provisioner.mjs
@@ -1,28 +1,24 @@
-import { execFileSync } from 'node:child_process';
-import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
-import { join, resolve } from 'node:path';
+import { execFileSync } from "node:child_process";
+import { existsSync, mkdirSync, copyFileSync } from "node:fs";
+import { join, resolve } from "node:path";
-if (process.platform !== 'win32') {
+if (process.platform !== "win32") {
process.exit(0);
}
const frontendDir = process.cwd();
-const tauriDir = resolve(frontendDir, 'src-tauri');
-const provisionerManifest = join(tauriDir, 'provisioner', 'Cargo.toml');
+const tauriDir = resolve(frontendDir, "src-tauri");
+const provisionerManifest = join(tauriDir, "provisioner", "Cargo.toml");
-execFileSync(
- 'cargo',
- ['build', '--release', '--manifest-path', provisionerManifest],
- { stdio: 'inherit' }
-);
+execFileSync("cargo", ["build", "--release", "--manifest-path", provisionerManifest], { stdio: "inherit" });
-const provisionerExe = join(tauriDir, 'provisioner', 'target', 'release', 'stirling-provisioner.exe');
+const provisionerExe = join(tauriDir, "provisioner", "target", "release", "stirling-provisioner.exe");
if (!existsSync(provisionerExe)) {
throw new Error(`Provisioner binary not found at ${provisionerExe}`);
}
-const wixDir = join(tauriDir, 'windows', 'wix');
+const wixDir = join(tauriDir, "windows", "wix");
mkdirSync(wixDir, { recursive: true });
-const destExe = join(wixDir, 'stirling-provision.exe');
+const destExe = join(wixDir, "stirling-provision.exe");
copyFileSync(provisionerExe, destExe);
diff --git a/frontend/scripts/generate-icons.js b/frontend/scripts/generate-icons.js
index 96566341c6..d15c53393d 100644
--- a/frontend/scripts/generate-icons.js
+++ b/frontend/scripts/generate-icons.js
@@ -1,11 +1,11 @@
#!/usr/bin/env node
-const { icons } = require('@iconify-json/material-symbols');
-const fs = require('fs');
-const path = require('path');
+const { icons } = require("@iconify-json/material-symbols");
+const fs = require("fs");
+const path = require("path");
// Check for verbose flag
-const isVerbose = process.argv.includes('--verbose') || process.argv.includes('-v');
+const isVerbose = process.argv.includes("--verbose") || process.argv.includes("-v");
// Logging functions
const info = (message) => console.log(message);
@@ -18,12 +18,12 @@ const debug = (message) => {
// Function to scan codebase for LocalIcon usage
function scanForUsedIcons() {
const usedIcons = new Set();
- const srcDir = path.join(__dirname, '..', 'src');
+ const srcDir = path.join(__dirname, "..", "src");
- info('š Scanning codebase for LocalIcon usage...');
+ info("š Scanning codebase for LocalIcon usage...");
if (!fs.existsSync(srcDir)) {
- console.error('ā Source directory not found:', srcDir);
+ console.error("ā Source directory not found:", srcDir);
process.exit(1);
}
@@ -31,19 +31,19 @@ function scanForUsedIcons() {
function scanDirectory(dir) {
const files = fs.readdirSync(dir);
- files.forEach(file => {
+ files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
scanDirectory(filePath);
- } else if (file.endsWith('.tsx') || file.endsWith('.ts')) {
- const content = fs.readFileSync(filePath, 'utf8');
+ } else if (file.endsWith(".tsx") || file.endsWith(".ts")) {
+ const content = fs.readFileSync(filePath, "utf8");
// Match LocalIcon usage:
const localIconMatches = content.match(/]*icon="([^"]+)"/g);
if (localIconMatches) {
- localIconMatches.forEach(match => {
+ localIconMatches.forEach((match) => {
const iconMatch = match.match(/icon="([^"]+)"/);
if (iconMatch) {
usedIcons.add(iconMatch[1]);
@@ -55,7 +55,7 @@ function scanForUsedIcons() {
// Match LocalIcon usage:
const localIconSingleQuoteMatches = content.match(/]*icon='([^']+)'/g);
if (localIconSingleQuoteMatches) {
- localIconSingleQuoteMatches.forEach(match => {
+ localIconSingleQuoteMatches.forEach((match) => {
const iconMatch = match.match(/icon='([^']+)'/);
if (iconMatch) {
usedIcons.add(iconMatch[1]);
@@ -67,7 +67,7 @@ function scanForUsedIcons() {
// Match old material-symbols-rounded spans: icon-name
const spanMatches = content.match(/]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g);
if (spanMatches) {
- spanMatches.forEach(match => {
+ spanMatches.forEach((match) => {
const iconMatch = match.match(/>([^<]+)<\/span>/);
if (iconMatch && iconMatch[1].trim()) {
const iconName = iconMatch[1].trim();
@@ -80,7 +80,7 @@ function scanForUsedIcons() {
// Match Icon component usage:
const iconMatches = content.match(/]*icon="material-symbols:([^"]+)"/g);
if (iconMatches) {
- iconMatches.forEach(match => {
+ iconMatches.forEach((match) => {
const iconMatch = match.match(/icon="material-symbols:([^"]+)"/);
if (iconMatch) {
usedIcons.add(iconMatch[1]);
@@ -92,7 +92,7 @@ function scanForUsedIcons() {
// Match icon config usage: icon: 'icon-name' or icon: "icon-name"
const iconPropertyMatches = content.match(/icon:\s*(['"])([a-z0-9-]+)\1/g);
if (iconPropertyMatches) {
- iconPropertyMatches.forEach(match => {
+ iconPropertyMatches.forEach((match) => {
const iconMatch = match.match(/icon:\s*(['"])([a-z0-9-]+)\1/);
if (iconMatch) {
usedIcons.add(iconMatch[2]);
@@ -118,18 +118,20 @@ async function main() {
const usedIcons = scanForUsedIcons();
// Check if we need to regenerate (compare with existing)
- const outputPath = path.join(__dirname, '..', 'src', 'assets', 'material-symbols-icons.json');
+ const outputPath = path.join(__dirname, "..", "src", "assets", "material-symbols-icons.json");
let needsRegeneration = true;
if (fs.existsSync(outputPath)) {
try {
- const existingSet = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
+ const existingSet = JSON.parse(fs.readFileSync(outputPath, "utf8"));
const existingIcons = Object.keys(existingSet.icons || {}).sort();
const currentIcons = [...usedIcons].sort();
if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) {
needsRegeneration = false;
- info(`ā
Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`);
+ info(
+ `ā
Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`,
+ );
}
} catch {
// If we can't parse existing file, regenerate
@@ -138,34 +140,34 @@ async function main() {
}
if (!needsRegeneration) {
- info('š No regeneration needed!');
+ info("š No regeneration needed!");
process.exit(0);
}
info(`š Extracting ${usedIcons.length} icons from Material Symbols...`);
// Dynamic import of ES module
- const { getIcons } = await import('@iconify/utils');
+ const { getIcons } = await import("@iconify/utils");
// Extract only our used icons from the full set
const extractedIcons = getIcons(icons, usedIcons);
if (!extractedIcons) {
- console.error('ā Failed to extract icons');
+ console.error("ā Failed to extract icons");
process.exit(1);
}
// Check for missing icons
const extractedIconNames = Object.keys(extractedIcons.icons || {});
- const missingIcons = usedIcons.filter(icon => !extractedIconNames.includes(icon));
+ const missingIcons = usedIcons.filter((icon) => !extractedIconNames.includes(icon));
if (missingIcons.length > 0) {
- info(`ā ļø Missing icons (${missingIcons.length}): ${missingIcons.join(', ')}`);
- info('š” These icons don\'t exist in Material Symbols. Please use available alternatives.');
+ info(`ā ļø Missing icons (${missingIcons.length}): ${missingIcons.join(", ")}`);
+ info("š” These icons don't exist in Material Symbols. Please use available alternatives.");
}
// Create output directory
- const outputDir = path.join(__dirname, '..', 'src', 'assets');
+ const outputDir = path.join(__dirname, "..", "src", "assets");
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
@@ -182,7 +184,7 @@ async function main() {
// This file is automatically generated by scripts/generate-icons.js
// Do not edit manually - changes will be overwritten
-export type MaterialSymbolIcon = ${usedIcons.map(icon => `'${icon}'`).join(' | ')};
+export type MaterialSymbolIcon = ${usedIcons.map((icon) => `'${icon}'`).join(" | ")};
export interface IconSet {
prefix: string;
@@ -196,7 +198,7 @@ declare const iconSet: IconSet;
export default iconSet;
`;
- const typesPath = path.join(outputDir, 'material-symbols-icons.d.ts');
+ const typesPath = path.join(outputDir, "material-symbols-icons.d.ts");
fs.writeFileSync(typesPath, typesContent);
info(`š Generated types: ${typesPath}`);
@@ -204,7 +206,7 @@ export default iconSet;
}
// Run the main function
-main().catch(error => {
- console.error('ā Script failed:', error);
+main().catch((error) => {
+ console.error("ā Script failed:", error);
process.exit(1);
});
diff --git a/frontend/scripts/generate-licenses.js b/frontend/scripts/generate-licenses.js
index e4b40c0e42..339e208326 100644
--- a/frontend/scripts/generate-licenses.js
+++ b/frontend/scripts/generate-licenses.js
@@ -1,11 +1,11 @@
#!/usr/bin/env node
-const { execSync } = require('node:child_process');
-const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('node:fs');
-const path = require('node:path');
+const { execSync } = require("node:child_process");
+const { existsSync, mkdirSync, writeFileSync, readFileSync } = require("node:fs");
+const path = require("node:path");
-const { argv } = require('node:process');
-const inputIdx = argv.indexOf('--input');
+const { argv } = require("node:process");
+const inputIdx = argv.indexOf("--input");
const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null;
const POSTPROCESS_ONLY = !!INPUT_FILE;
@@ -16,408 +16,434 @@ const POSTPROCESS_ONLY = !!INPUT_FILE;
* This script creates a JSON file similar to the Java backend's 3rdPartyLicenses.json
*/
-const OUTPUT_FILE = path.join(__dirname, '..', 'src', 'assets', '3rdPartyLicenses.json');
-const PACKAGE_JSON = path.join(__dirname, '..', 'package.json');
+const OUTPUT_FILE = path.join(__dirname, "..", "src", "assets", "3rdPartyLicenses.json");
+const PACKAGE_JSON = path.join(__dirname, "..", "package.json");
// Ensure the output directory exists
const outputDir = path.dirname(OUTPUT_FILE);
if (!existsSync(outputDir)) {
- mkdirSync(outputDir, { recursive: true });
+ mkdirSync(outputDir, { recursive: true });
}
-console.log('š Generating frontend license report...');
+console.log("š Generating frontend license report...");
try {
- // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK)
- if (process.env.PR_IS_FORK === 'true' && !POSTPROCESS_ONLY) {
- console.error('Fork PR detected: only --input (postprocess-only) mode is allowed.');
- process.exit(2);
+ // Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK)
+ if (process.env.PR_IS_FORK === "true" && !POSTPROCESS_ONLY) {
+ console.error("Fork PR detected: only --input (postprocess-only) mode is allowed.");
+ process.exit(2);
+ }
+
+ let licenseData;
+ // Generate license report using pinned license-checker; disable lifecycle scripts
+ if (POSTPROCESS_ONLY) {
+ if (!INPUT_FILE || !existsSync(INPUT_FILE)) {
+ console.error("ā --input file missing or not found");
+ process.exit(1);
}
-
- let licenseData;
- // Generate license report using pinned license-checker; disable lifecycle scripts
- if (POSTPROCESS_ONLY) {
- if (!INPUT_FILE || !existsSync(INPUT_FILE)) {
- console.error('ā --input file missing or not found');
- process.exit(1);
- }
- licenseData = JSON.parse(readFileSync(INPUT_FILE, 'utf8'));
- } else {
- const licenseReport = execSync(
- // 'npx --yes license-checker@25.0.1 --production --json',
- 'npx --yes license-report --only=prod --output=json',
- {
- encoding: 'utf8',
- cwd: path.dirname(PACKAGE_JSON),
- env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: 'true' }
- }
- );
- try {
- licenseData = JSON.parse(licenseReport);
- } catch (parseError) {
- console.error('ā Failed to parse license data:', parseError.message);
- console.error('Raw output:', licenseReport.substring(0, 500) + '...');
- process.exit(1);
- }
- }
-
- if (!Array.isArray(licenseData)) {
- console.error('ā Invalid license data structure');
- process.exit(1);
- }
-
- // Convert license-checker format to array
- const licenseArray = licenseData.map(dep => {
- let licenseType = dep.licenseType;
-
- // Handle missing or null licenses
- if (!licenseType || licenseType === null || licenseType === undefined) {
- licenseType = 'Unknown';
- }
-
- // Handle empty string licenses
- if (licenseType === '') {
- licenseType = 'Unknown';
- }
-
- // Handle array licenses (rare but possible)
- if (Array.isArray(licenseType)) {
- licenseType = licenseType.join(' AND ');
- }
-
- // Handle object licenses (fallback)
- if (typeof licenseType === 'object' && licenseType !== null) {
- licenseType = 'Unknown';
- }
-
- if ( "posthog-js" === dep.name && licenseType.startsWith("SEE LICENSE IN LICENSE")) {
- licenseType = "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE";
- }
-
- return {
- name: dep.name,
- version: dep.installedVersion || dep.definedVersion || dep.remoteVersion || 'unknown',
- licenseType: licenseType,
- repository: dep.link,
- url: dep.link,
- link: dep.link
- };
- });
-
- // Transform to match Java backend format
- const transformedData = {
- dependencies: licenseArray.map(dep => {
- const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown');
- const licenseUrl = dep.link || getLicenseUrl(licenseType);
-
- return {
- moduleName: dep.name,
- moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`,
- moduleVersion: dep.version,
- moduleLicense: licenseType,
- moduleLicenseUrl: licenseUrl
- };
- })
- };
-
- // Log summary of license types found
- const licenseSummary = licenseArray.reduce((acc, dep) => {
- const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown');
- acc[license] = (acc[license] || 0) + 1;
- return acc;
- }, {});
-
- console.log('š License types found:');
- Object.entries(licenseSummary).forEach(([license, count]) => {
- console.log(` ${license}: ${count} packages`);
- });
-
- // Log any complex or unusual license formats for debugging
- const complexLicenses = licenseArray.filter(dep =>
- dep.licenseType && (
- dep.licenseType.includes('AND') ||
- dep.licenseType.includes('OR') ||
- dep.licenseType === 'Unknown' ||
- dep.licenseType.includes('SEE LICENSE')
- )
+ licenseData = JSON.parse(readFileSync(INPUT_FILE, "utf8"));
+ } else {
+ const licenseReport = execSync(
+ // 'npx --yes license-checker@25.0.1 --production --json',
+ "npx --yes license-report --only=prod --output=json",
+ {
+ encoding: "utf8",
+ cwd: path.dirname(PACKAGE_JSON),
+ env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: "true" },
+ },
);
-
- if (complexLicenses.length > 0) {
- console.log('\nš Complex/Edge case licenses detected:');
- complexLicenses.forEach(dep => {
- console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`);
- });
+ try {
+ licenseData = JSON.parse(licenseReport);
+ } catch (parseError) {
+ console.error("ā Failed to parse license data:", parseError.message);
+ console.error("Raw output:", licenseReport.substring(0, 500) + "...");
+ process.exit(1);
}
+ }
- // Check for potentially problematic licenses
- const problematicLicenses = checkLicenseCompatibility(licenseSummary, licenseArray);
- if (problematicLicenses.length > 0) {
- console.log('\nā ļø License compatibility warnings:');
- problematicLicenses.forEach(warning => {
- console.log(` ${warning.message}`);
- });
-
- // Write license warnings to a separate file for CI/CD
- const warningsFile = path.join(__dirname, '..', 'src', 'assets', 'license-warnings.json');
- writeFileSync(warningsFile, JSON.stringify({
- warnings: problematicLicenses,
- generated: new Date().toISOString()
- }, null, 2));
- console.log(`ā ļø License warnings saved to: ${warningsFile}`);
- } else {
- console.log('\nā
All licenses appear to be corporate-friendly');
- }
-
- // Write to file
- writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 4));
-
- console.log(`ā
License report generated successfully!`);
- console.log(`š Found ${transformedData.dependencies.length} dependencies`);
- console.log(`š¾ Saved to: ${OUTPUT_FILE}`);
-
-} catch (error) {
- console.error('ā Error generating license report:', error.message);
+ if (!Array.isArray(licenseData)) {
+ console.error("ā Invalid license data structure");
process.exit(1);
+ }
+
+ // Convert license-checker format to array
+ const licenseArray = licenseData.map((dep) => {
+ let licenseType = dep.licenseType;
+
+ // Handle missing or null licenses
+ if (!licenseType || licenseType === null || licenseType === undefined) {
+ licenseType = "Unknown";
+ }
+
+ // Handle empty string licenses
+ if (licenseType === "") {
+ licenseType = "Unknown";
+ }
+
+ // Handle array licenses (rare but possible)
+ if (Array.isArray(licenseType)) {
+ licenseType = licenseType.join(" AND ");
+ }
+
+ // Handle object licenses (fallback)
+ if (typeof licenseType === "object" && licenseType !== null) {
+ licenseType = "Unknown";
+ }
+
+ if ("posthog-js" === dep.name && licenseType.startsWith("SEE LICENSE IN LICENSE")) {
+ licenseType = "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE";
+ }
+
+ return {
+ name: dep.name,
+ version: dep.installedVersion || dep.definedVersion || dep.remoteVersion || "unknown",
+ licenseType: licenseType,
+ repository: dep.link,
+ url: dep.link,
+ link: dep.link,
+ };
+ });
+
+ // Transform to match Java backend format
+ const transformedData = {
+ dependencies: licenseArray.map((dep) => {
+ const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType || "Unknown";
+ const licenseUrl = dep.link || getLicenseUrl(licenseType);
+
+ return {
+ moduleName: dep.name,
+ moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`,
+ moduleVersion: dep.version,
+ moduleLicense: licenseType,
+ moduleLicenseUrl: licenseUrl,
+ };
+ }),
+ };
+
+ // Log summary of license types found
+ const licenseSummary = licenseArray.reduce((acc, dep) => {
+ const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType || "Unknown";
+ acc[license] = (acc[license] || 0) + 1;
+ return acc;
+ }, {});
+
+ console.log("š License types found:");
+ Object.entries(licenseSummary).forEach(([license, count]) => {
+ console.log(` ${license}: ${count} packages`);
+ });
+
+ // Log any complex or unusual license formats for debugging
+ const complexLicenses = licenseArray.filter(
+ (dep) =>
+ dep.licenseType &&
+ (dep.licenseType.includes("AND") ||
+ dep.licenseType.includes("OR") ||
+ dep.licenseType === "Unknown" ||
+ dep.licenseType.includes("SEE LICENSE")),
+ );
+
+ if (complexLicenses.length > 0) {
+ console.log("\nš Complex/Edge case licenses detected:");
+ complexLicenses.forEach((dep) => {
+ console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`);
+ });
+ }
+
+ // Check for potentially problematic licenses
+ const problematicLicenses = checkLicenseCompatibility(licenseSummary, licenseArray);
+ if (problematicLicenses.length > 0) {
+ console.log("\nā ļø License compatibility warnings:");
+ problematicLicenses.forEach((warning) => {
+ console.log(` ${warning.message}`);
+ });
+
+ // Write license warnings to a separate file for CI/CD
+ const warningsFile = path.join(__dirname, "..", "src", "assets", "license-warnings.json");
+ writeFileSync(
+ warningsFile,
+ JSON.stringify(
+ {
+ warnings: problematicLicenses,
+ generated: new Date().toISOString(),
+ },
+ null,
+ 2,
+ ),
+ );
+ console.log(`ā ļø License warnings saved to: ${warningsFile}`);
+ } else {
+ console.log("\nā
All licenses appear to be corporate-friendly");
+ }
+
+ // Write to file
+ writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 2) + "\n");
+
+ console.log(`ā
License report generated successfully!`);
+ console.log(`š Found ${transformedData.dependencies.length} dependencies`);
+ console.log(`š¾ Saved to: ${OUTPUT_FILE}`);
+} catch (error) {
+ console.error("ā Error generating license report:", error.message);
+ process.exit(1);
}
/**
* Get standard license URLs for common licenses
*/
function getLicenseUrl(licenseType) {
- if (!licenseType || licenseType === 'Unknown') return '';
+ if (!licenseType || licenseType === "Unknown") return "";
- const licenseUrls = {
- 'MIT': 'https://opensource.org/licenses/MIT',
- 'MIT*': 'https://opensource.org/licenses/MIT',
- 'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
- 'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0',
- 'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause',
- 'BSD-2-Clause': 'https://opensource.org/licenses/BSD-2-Clause',
- 'BSD': 'https://opensource.org/licenses/BSD-3-Clause',
- 'GPL-3.0': 'https://www.gnu.org/licenses/gpl-3.0.html',
- 'GPL-2.0': 'https://www.gnu.org/licenses/gpl-2.0.html',
- 'LGPL-2.1': 'https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html',
- 'LGPL-3.0': 'https://www.gnu.org/licenses/lgpl-3.0.html',
- 'ISC': 'https://opensource.org/licenses/ISC',
- 'CC0-1.0': 'https://creativecommons.org/publicdomain/zero/1.0/',
- 'Unlicense': 'https://unlicense.org/',
- 'MPL-2.0': 'https://www.mozilla.org/en-US/MPL/2.0/',
- 'WTFPL': 'http://www.wtfpl.net/',
- 'Zlib': 'https://opensource.org/licenses/Zlib',
- 'Artistic-2.0': 'https://opensource.org/licenses/Artistic-2.0',
- 'EPL-1.0': 'https://www.eclipse.org/legal/epl-v10.html',
- 'EPL-2.0': 'https://www.eclipse.org/legal/epl-2.0/',
- 'CDDL-1.0': 'https://opensource.org/licenses/CDDL-1.0',
- 'Ruby': 'https://www.ruby-lang.org/en/about/license.txt',
- 'Python-2.0': 'https://www.python.org/download/releases/2.0/license/',
- 'Public Domain': 'https://creativecommons.org/publicdomain/zero/1.0/',
- 'UNLICENSED': ''
- };
+ const licenseUrls = {
+ MIT: "https://opensource.org/licenses/MIT",
+ "MIT*": "https://opensource.org/licenses/MIT",
+ "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0",
+ "Apache License 2.0": "https://www.apache.org/licenses/LICENSE-2.0",
+ "BSD-3-Clause": "https://opensource.org/licenses/BSD-3-Clause",
+ "BSD-2-Clause": "https://opensource.org/licenses/BSD-2-Clause",
+ BSD: "https://opensource.org/licenses/BSD-3-Clause",
+ "GPL-3.0": "https://www.gnu.org/licenses/gpl-3.0.html",
+ "GPL-2.0": "https://www.gnu.org/licenses/gpl-2.0.html",
+ "LGPL-2.1": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html",
+ "LGPL-3.0": "https://www.gnu.org/licenses/lgpl-3.0.html",
+ ISC: "https://opensource.org/licenses/ISC",
+ "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/",
+ Unlicense: "https://unlicense.org/",
+ "MPL-2.0": "https://www.mozilla.org/en-US/MPL/2.0/",
+ WTFPL: "http://www.wtfpl.net/",
+ Zlib: "https://opensource.org/licenses/Zlib",
+ "Artistic-2.0": "https://opensource.org/licenses/Artistic-2.0",
+ "EPL-1.0": "https://www.eclipse.org/legal/epl-v10.html",
+ "EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/",
+ "CDDL-1.0": "https://opensource.org/licenses/CDDL-1.0",
+ Ruby: "https://www.ruby-lang.org/en/about/license.txt",
+ "Python-2.0": "https://www.python.org/download/releases/2.0/license/",
+ "Public Domain": "https://creativecommons.org/publicdomain/zero/1.0/",
+ UNLICENSED: "",
+ };
- // Try exact match first
- if (licenseUrls[licenseType]) {
- return licenseUrls[licenseType];
+ // Try exact match first
+ if (licenseUrls[licenseType]) {
+ return licenseUrls[licenseType];
+ }
+
+ // Try case-insensitive match
+ const lowerType = licenseType.toLowerCase();
+ for (const [key, url] of Object.entries(licenseUrls)) {
+ if (key.toLowerCase() === lowerType) {
+ return url;
}
+ }
- // Try case-insensitive match
- const lowerType = licenseType.toLowerCase();
- for (const [key, url] of Object.entries(licenseUrls)) {
- if (key.toLowerCase() === lowerType) {
- return url;
- }
+ // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
+ if (licenseType.includes("AND") || licenseType.includes("OR")) {
+ // Extract the first license from compound expressions for URL
+ const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/);
+ if (match && licenseUrls[match[1]]) {
+ return licenseUrls[match[1]];
}
+ }
- // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
- if (licenseType.includes('AND') || licenseType.includes('OR')) {
- // Extract the first license from compound expressions for URL
- const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/);
- if (match && licenseUrls[match[1]]) {
- return licenseUrls[match[1]];
- }
- }
-
- // For non-standard licenses, return empty string (will use package link if available)
- return '';
+ // For non-standard licenses, return empty string (will use package link if available)
+ return "";
}
/**
* Check for potentially problematic licenses that may not be MIT/corporate compatible
*/
function checkLicenseCompatibility(licenseSummary, licenseArray) {
- const warnings = [];
+ const warnings = [];
- // Define problematic license patterns
- const problematicLicenses = {
- // Copyleft licenses
- 'GPL-2.0': 'Strong copyleft license - requires derivative works to be GPL',
- 'GPL-3.0': 'Strong copyleft license - requires derivative works to be GPL',
- 'LGPL-2.1': 'Weak copyleft license - may require source disclosure for modifications',
- 'LGPL-3.0': 'Weak copyleft license - may require source disclosure for modifications',
- 'AGPL-3.0': 'Network copyleft license - requires source disclosure for network use',
- 'AGPL-1.0': 'Network copyleft license - requires source disclosure for network use',
+ // Define problematic license patterns
+ const problematicLicenses = {
+ // Copyleft licenses
+ "GPL-2.0": "Strong copyleft license - requires derivative works to be GPL",
+ "GPL-3.0": "Strong copyleft license - requires derivative works to be GPL",
+ "LGPL-2.1": "Weak copyleft license - may require source disclosure for modifications",
+ "LGPL-3.0": "Weak copyleft license - may require source disclosure for modifications",
+ "AGPL-3.0": "Network copyleft license - requires source disclosure for network use",
+ "AGPL-1.0": "Network copyleft license - requires source disclosure for network use",
- // Other potentially problematic licenses
- 'WTFPL': 'Potentially problematic license - legal uncertainty',
- 'CC-BY-SA-4.0': 'ShareAlike license - requires derivative works to use same license',
- 'CC-BY-SA-3.0': 'ShareAlike license - requires derivative works to use same license',
- 'CC-BY-NC-4.0': 'Non-commercial license - prohibits commercial use',
- 'CC-BY-NC-3.0': 'Non-commercial license - prohibits commercial use',
- 'OSL-3.0': 'Copyleft license - requires derivative works to be OSL',
- 'EPL-1.0': 'Weak copyleft license - may require source disclosure',
- 'EPL-2.0': 'Weak copyleft license - may require source disclosure',
- 'CDDL-1.0': 'Weak copyleft license - may require source disclosure',
- 'CDDL-1.1': 'Weak copyleft license - may require source disclosure',
- 'CPL-1.0': 'Weak copyleft license - may require source disclosure',
- 'MPL-1.1': 'Weak copyleft license - may require source disclosure',
- 'EUPL-1.1': 'Copyleft license - requires derivative works to be EUPL',
- 'EUPL-1.2': 'Copyleft license - requires derivative works to be EUPL',
- 'UNLICENSED': 'No license specified - usage rights unclear',
- 'Unknown': 'License not detected - manual review required'
- };
+ // Other potentially problematic licenses
+ WTFPL: "Potentially problematic license - legal uncertainty",
+ "CC-BY-SA-4.0": "ShareAlike license - requires derivative works to use same license",
+ "CC-BY-SA-3.0": "ShareAlike license - requires derivative works to use same license",
+ "CC-BY-NC-4.0": "Non-commercial license - prohibits commercial use",
+ "CC-BY-NC-3.0": "Non-commercial license - prohibits commercial use",
+ "OSL-3.0": "Copyleft license - requires derivative works to be OSL",
+ "EPL-1.0": "Weak copyleft license - may require source disclosure",
+ "EPL-2.0": "Weak copyleft license - may require source disclosure",
+ "CDDL-1.0": "Weak copyleft license - may require source disclosure",
+ "CDDL-1.1": "Weak copyleft license - may require source disclosure",
+ "CPL-1.0": "Weak copyleft license - may require source disclosure",
+ "MPL-1.1": "Weak copyleft license - may require source disclosure",
+ "EUPL-1.1": "Copyleft license - requires derivative works to be EUPL",
+ "EUPL-1.2": "Copyleft license - requires derivative works to be EUPL",
+ UNLICENSED: "No license specified - usage rights unclear",
+ Unknown: "License not detected - manual review required",
+ };
- // Known good licenses (no warnings needed)
- const goodLicenses = new Set([
- 'MIT', 'MIT*', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD',
- 'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0',
- 'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0',
- 'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE',
- 'SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE'
- ]);
+ // Known good licenses (no warnings needed)
+ const goodLicenses = new Set([
+ "MIT",
+ "MIT*",
+ "Apache-2.0",
+ "Apache License 2.0",
+ "BSD-2-Clause",
+ "BSD-3-Clause",
+ "BSD",
+ "ISC",
+ "CC0-1.0",
+ "Public Domain",
+ "Unlicense",
+ "0BSD",
+ "BlueOak-1.0.0",
+ "Zlib",
+ "Artistic-2.0",
+ "Python-2.0",
+ "Ruby",
+ "MPL-2.0",
+ "CC-BY-4.0",
+ "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
+ "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE",
+ ]);
- // Helper function to normalize license names for comparison
- function normalizeLicense(license) {
- return license
- .replace(/-or-later$/, '') // Remove -or-later suffix
- .replace(/\+$/, '') // Remove + suffix
- .trim();
+ // Helper function to normalize license names for comparison
+ function normalizeLicense(license) {
+ return license
+ .replace(/-or-later$/, "") // Remove -or-later suffix
+ .replace(/\+$/, "") // Remove + suffix
+ .trim();
+ }
+
+ // Check each license type
+ Object.entries(licenseSummary).forEach(([license, count]) => {
+ // Skip known good licenses
+ if (goodLicenses.has(license)) {
+ return;
}
- // Check each license type
- Object.entries(licenseSummary).forEach(([license, count]) => {
- // Skip known good licenses
- if (goodLicenses.has(license)) {
- return;
- }
-
- // Check if this license only affects our own packages
- const affectedPackages = licenseArray.filter(dep => {
- const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType;
- return depLicense === license;
- });
-
- const isOnlyOurPackages = affectedPackages.every(dep =>
- dep.name === 'frontend' ||
- dep.name.toLowerCase().includes('stirling-pdf') ||
- dep.name.toLowerCase().includes('stirling_pdf') ||
- dep.name.toLowerCase().includes('stirlingpdf')
- );
-
- if (isOnlyOurPackages && (license === 'UNLICENSED' || license.startsWith('SEE LICENSE IN'))) {
- return; // Skip warnings for our own Stirling-PDF packages
- }
-
- // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
- if (license.includes('AND') || license.includes('OR')) {
- // For OR licenses, check if there's at least one acceptable license option
- if (license.includes('OR')) {
- // Extract license components from OR expression
- const orComponents = license
- .replace(/[()]/g, '') // Remove parentheses
- .split(' OR ')
- .map(component => component.trim());
-
- // Check if any component is in the goodLicenses set (with normalization)
- const hasGoodLicense = orComponents.some(component => {
- const normalized = normalizeLicense(component);
- return goodLicenses.has(component) || goodLicenses.has(normalized);
- });
-
- if (hasGoodLicense) {
- return; // Skip warning - can use the good license option
- }
- }
-
- // For AND licenses or OR licenses with no good options, check for problematic components
- const hasProblematicComponent = Object.keys(problematicLicenses).some(problematic =>
- license.includes(problematic)
- );
-
- if (hasProblematicComponent) {
- const affectedPackages = licenseArray
- .filter(dep => {
- const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType;
- return depLicense === license;
- })
- .map(dep => ({
- name: dep.name,
- version: dep.version,
- url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`
- }));
-
- const licenseType = license.includes('AND') ? 'AND' : 'OR';
- const reason = licenseType === 'AND'
- ? 'Compound license with AND requirement - all components must be compatible'
- : 'Compound license with potentially problematic components and no good fallback options';
-
- warnings.push({
- message: `š This PR contains ${count} package${count > 1 ? 's' : ''} with compound license "${license}" - manual review recommended`,
- licenseType: license,
- licenseUrl: '',
- reason: reason,
- packageCount: count,
- affectedDependencies: affectedPackages
- });
- }
- return;
- }
-
- // Check for exact matches with problematic licenses
- if (problematicLicenses[license]) {
- const affectedPackages = licenseArray
- .filter(dep => {
- const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType;
- return depLicense === license;
- })
- .map(dep => ({
- name: dep.name,
- version: dep.version,
- url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`
- }));
-
- const packageList = affectedPackages.map(pkg => pkg.name).slice(0, 5).join(', ') + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : '');
- const licenseUrl = getLicenseUrl(license) || 'https://opensource.org/licenses';
-
- warnings.push({
- message: `ā ļø This PR contains ${count} package${count > 1 ? 's' : ''} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`,
- licenseType: license,
- licenseUrl: licenseUrl,
- reason: problematicLicenses[license],
- packageCount: count,
- affectedDependencies: affectedPackages
- });
- } else {
- // Unknown license type - flag for manual review
- const affectedPackages = licenseArray
- .filter(dep => {
- const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType;
- return depLicense === license;
- })
- .map(dep => ({
- name: dep.name,
- version: dep.version,
- url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`
- }));
-
- warnings.push({
- message: `ā This PR contains ${count} package${count > 1 ? 's' : ''} with unknown license type "${license}" - manual review required`,
- licenseType: license,
- licenseUrl: '',
- reason: 'Unknown license type',
- packageCount: count,
- affectedDependencies: affectedPackages
- });
- }
+ // Check if this license only affects our own packages
+ const affectedPackages = licenseArray.filter((dep) => {
+ const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType;
+ return depLicense === license;
});
- return warnings;
+ const isOnlyOurPackages = affectedPackages.every(
+ (dep) =>
+ dep.name === "frontend" ||
+ dep.name.toLowerCase().includes("stirling-pdf") ||
+ dep.name.toLowerCase().includes("stirling_pdf") ||
+ dep.name.toLowerCase().includes("stirlingpdf"),
+ );
+
+ if (isOnlyOurPackages && (license === "UNLICENSED" || license.startsWith("SEE LICENSE IN"))) {
+ return; // Skip warnings for our own Stirling-PDF packages
+ }
+
+ // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
+ if (license.includes("AND") || license.includes("OR")) {
+ // For OR licenses, check if there's at least one acceptable license option
+ if (license.includes("OR")) {
+ // Extract license components from OR expression
+ const orComponents = license
+ .replace(/[()]/g, "") // Remove parentheses
+ .split(" OR ")
+ .map((component) => component.trim());
+
+ // Check if any component is in the goodLicenses set (with normalization)
+ const hasGoodLicense = orComponents.some((component) => {
+ const normalized = normalizeLicense(component);
+ return goodLicenses.has(component) || goodLicenses.has(normalized);
+ });
+
+ if (hasGoodLicense) {
+ return; // Skip warning - can use the good license option
+ }
+ }
+
+ // For AND licenses or OR licenses with no good options, check for problematic components
+ const hasProblematicComponent = Object.keys(problematicLicenses).some((problematic) => license.includes(problematic));
+
+ if (hasProblematicComponent) {
+ const affectedPackages = licenseArray
+ .filter((dep) => {
+ const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType;
+ return depLicense === license;
+ })
+ .map((dep) => ({
+ name: dep.name,
+ version: dep.version,
+ url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`,
+ }));
+
+ const licenseType = license.includes("AND") ? "AND" : "OR";
+ const reason =
+ licenseType === "AND"
+ ? "Compound license with AND requirement - all components must be compatible"
+ : "Compound license with potentially problematic components and no good fallback options";
+
+ warnings.push({
+ message: `š This PR contains ${count} package${count > 1 ? "s" : ""} with compound license "${license}" - manual review recommended`,
+ licenseType: license,
+ licenseUrl: "",
+ reason: reason,
+ packageCount: count,
+ affectedDependencies: affectedPackages,
+ });
+ }
+ return;
+ }
+
+ // Check for exact matches with problematic licenses
+ if (problematicLicenses[license]) {
+ const affectedPackages = licenseArray
+ .filter((dep) => {
+ const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType;
+ return depLicense === license;
+ })
+ .map((dep) => ({
+ name: dep.name,
+ version: dep.version,
+ url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`,
+ }));
+
+ const packageList =
+ affectedPackages
+ .map((pkg) => pkg.name)
+ .slice(0, 5)
+ .join(", ") + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : "");
+ const licenseUrl = getLicenseUrl(license) || "https://opensource.org/licenses";
+
+ warnings.push({
+ message: `ā ļø This PR contains ${count} package${count > 1 ? "s" : ""} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`,
+ licenseType: license,
+ licenseUrl: licenseUrl,
+ reason: problematicLicenses[license],
+ packageCount: count,
+ affectedDependencies: affectedPackages,
+ });
+ } else {
+ // Unknown license type - flag for manual review
+ const affectedPackages = licenseArray
+ .filter((dep) => {
+ const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(", ") : dep.licenseType;
+ return depLicense === license;
+ })
+ .map((dep) => ({
+ name: dep.name,
+ version: dep.version,
+ url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`,
+ }));
+
+ warnings.push({
+ message: `ā This PR contains ${count} package${count > 1 ? "s" : ""} with unknown license type "${license}" - manual review required`,
+ licenseType: license,
+ licenseUrl: "",
+ reason: "Unknown license type",
+ packageCount: count,
+ affectedDependencies: affectedPackages,
+ });
+ }
+ });
+
+ return warnings;
}
diff --git a/frontend/scripts/sample-pdf/generate.mjs b/frontend/scripts/sample-pdf/generate.mjs
index 93e5cf7ee3..2ad477cc98 100755
--- a/frontend/scripts/sample-pdf/generate.mjs
+++ b/frontend/scripts/sample-pdf/generate.mjs
@@ -8,20 +8,20 @@
* for users to experiment with Stirling PDF's features.
*/
-import puppeteer from 'puppeteer';
-import { fileURLToPath } from 'url';
-import { dirname, join } from 'path';
-import { existsSync, mkdirSync, statSync } from 'fs';
+import puppeteer from "puppeteer";
+import { fileURLToPath } from "url";
+import { dirname, join } from "path";
+import { existsSync, mkdirSync, statSync } from "fs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
-const TEMPLATE_PATH = join(__dirname, 'template.html');
-const OUTPUT_DIR = join(__dirname, '../../public/samples');
-const OUTPUT_PATH = join(OUTPUT_DIR, 'Sample.pdf');
+const TEMPLATE_PATH = join(__dirname, "template.html");
+const OUTPUT_DIR = join(__dirname, "../../public/samples");
+const OUTPUT_PATH = join(OUTPUT_DIR, "Sample.pdf");
async function generatePDF() {
- console.log('š Starting Stirling PDF sample document generation...\n');
+ console.log("š Starting Stirling PDF sample document generation...\n");
// Ensure output directory exists
if (!existsSync(OUTPUT_DIR)) {
@@ -40,66 +40,65 @@ async function generatePDF() {
let browser;
try {
// Launch Puppeteer
- console.log('š Launching browser...');
+ console.log("š Launching browser...");
browser = await puppeteer.launch({
- headless: 'new',
- args: ['--no-sandbox', '--disable-setuid-sandbox']
+ headless: "new",
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
// Set viewport to match A4 proportions
await page.setViewport({
- width: 794, // A4 width in pixels at 96 DPI
+ width: 794, // A4 width in pixels at 96 DPI
height: 1123, // A4 height in pixels at 96 DPI
- deviceScaleFactor: 2 // Higher quality rendering
+ deviceScaleFactor: 2, // Higher quality rendering
});
// Navigate to the template file
const fileUrl = `file://${TEMPLATE_PATH}`;
- console.log('š Loading HTML template...');
+ console.log("š Loading HTML template...");
await page.goto(fileUrl, {
- waitUntil: 'networkidle0' // Wait for all resources to load
+ waitUntil: "networkidle0", // Wait for all resources to load
});
// Generate PDF with A4 dimensions
- console.log('š Generating PDF...');
+ console.log("š Generating PDF...");
await page.pdf({
path: OUTPUT_PATH,
- format: 'A4',
+ format: "A4",
printBackground: true,
margin: {
top: 0,
right: 0,
bottom: 0,
- left: 0
+ left: 0,
},
- preferCSSPageSize: true
+ preferCSSPageSize: true,
});
- console.log('\nā
PDF generated successfully!');
+ console.log("\nā
PDF generated successfully!");
console.log(`š¦ Output: ${OUTPUT_PATH}`);
// Get file size
const stats = statSync(OUTPUT_PATH);
const fileSizeInKB = (stats.size / 1024).toFixed(2);
console.log(`š File size: ${fileSizeInKB} KB`);
-
} catch (error) {
- console.error('\nā Error generating PDF:', error.message);
+ console.error("\nā Error generating PDF:", error.message);
process.exit(1);
} finally {
if (browser) {
await browser.close();
- console.log('š Browser closed.');
+ console.log("š Browser closed.");
}
}
- console.log('\nš Done! Sample PDF is ready for use in Stirling PDF.\n');
+ console.log("\nš Done! Sample PDF is ready for use in Stirling PDF.\n");
}
// Run the generator
-generatePDF().catch(error => {
- console.error('Fatal error:', error);
+generatePDF().catch((error) => {
+ console.error("Fatal error:", error);
process.exit(1);
});
diff --git a/frontend/scripts/sample-pdf/styles.css b/frontend/scripts/sample-pdf/styles.css
index 067452833c..7f34b95e8f 100644
--- a/frontend/scripts/sample-pdf/styles.css
+++ b/frontend/scripts/sample-pdf/styles.css
@@ -20,8 +20,9 @@
--color-white: #ffffff;
/* Font Stack */
- --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ --font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
+ "Helvetica Neue", sans-serif;
}
* {
diff --git a/frontend/scripts/sample-pdf/template.html b/frontend/scripts/sample-pdf/template.html
index edd7f2c9f4..aea5e4cb13 100644
--- a/frontend/scripts/sample-pdf/template.html
+++ b/frontend/scripts/sample-pdf/template.html
@@ -1,234 +1,244 @@
-
+
-
-
-
- Stirling PDF - Sample Document
-
-
-
-
-
-
-
-
-
+
+
+
+
Stirling PDF - Sample Document
+
+
+
+
+
+
-
The Free Adobe Acrobat Alternative
-
-
-
10M+
-
Downloads
+
+
+
-
-
-
Open Source
-
Privacy First
-
Self-Hosted
-
-
-
-
-
-
-
-
What is Stirling PDF?
-
- Stirling PDF is a robust, web-based PDF manipulation tool.
- It enables you to carry out various operations on PDF files, including splitting,
- merging, converting, rearranging, adding images, rotating, compressing, and more.
-
-
-
-
-
-
-
-
+
The Free Adobe Acrobat Alternative
+
+
+ 10M+
+ Downloads
-
50+ PDF Operations
-
Comprehensive toolkit covering all your PDF needs. From basic operations to advanced processing.
-
-
-
-
Workflow Automation
-
Chain multiple operations together and save them as reusable workflows. Perfect for recurring tasks.
-
-
-
-
-
Multi-Language Support
-
Available in over 30 languages with community-contributed translations. Accessible to users worldwide.
-
-
-
-
-
Privacy First
-
Self-hosted solution means your data stays on your infrastructure. You have full control over your documents.
-
-
-
-
-
Open Source
-
Transparent, community-driven development. Inspect the code, contribute features, and adapt as needed.
-
-
-
-
-
API Access
-
RESTful API for integration with external tools and scripts. Automate PDF operations programmatically.
+
+
Open Source
+
Privacy First
+
Self-Hosted
-
-
-
-
-
Key Features
+
+
+
+
What is Stirling PDF?
+
+ Stirling PDF is a robust, web-based PDF manipulation tool. It enables you to carry out various operations on PDF
+ files, including splitting, merging, converting, rearranging, adding images, rotating, compressing, and more.
+
-