@jojomatik/nuxt-bundle 2.0.0-beta.3 → 2.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/app/components/CLicenseCard.stories.ts +21 -0
- package/app/pages/license-information.vue +2 -2
- package/lib/license-plugin-options.ts +104 -0
- package/lib/license-validation.ts +44 -0
- package/modules/favicon-module.ts +223 -0
- package/modules/license-module.ts +2 -73
- package/nuxt.config.ts +12 -1
- package/package.json +50 -35
- package/public/favicon.svg +1 -0
- package/vuetify-options.ts +48 -0
package/README.md
CHANGED
|
@@ -3,5 +3,62 @@
|
|
|
3
3
|
|
|
4
4
|
A bundle of commonly used nuxt dependencies and configuration.
|
|
5
5
|
|
|
6
|
+
## Usage
|
|
7
|
+
Extend your Nuxt configuration with `@jojomatik/nuxt-bundle` to use its shared dependencies and configuration. Dependency licenses are validated during builds.
|
|
8
|
+
|
|
9
|
+
Use `licenseModule.licenseOverrides` to provide license metadata for dependencies whose manifests omit it. Overrides are still checked against `licenseModule.allowedLicenses`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export default defineNuxtConfig({
|
|
13
|
+
extends: "@jojomatik/nuxt-bundle",
|
|
14
|
+
licenseModule: {
|
|
15
|
+
licenseOverrides: {
|
|
16
|
+
"socket.io-client": "MIT",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Dependencies without an override still fail validation when their license metadata is missing.
|
|
23
|
+
An override can also be a function that receives the license from the package metadata. For example, to supply MIT only when the license is missing:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
licenseOverrides: {
|
|
27
|
+
"socket.io-client": (license) => license || "MIT",
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Use an object in `allowedLicenses` to allow a license only for specific dependencies:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
licenseModule: {
|
|
35
|
+
allowedLicenses: [
|
|
36
|
+
"MIT",
|
|
37
|
+
{ "SEE LICENSE IN LICENSE.md": ["apexcharts", "vue-apexcharts"] },
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Favicons
|
|
43
|
+
The favicon module is opt-in. When enabled, it generates all favicon files from a single SVG during the build:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export default defineNuxtConfig({
|
|
47
|
+
extends: "@jojomatik/nuxt-bundle",
|
|
48
|
+
faviconModule: {
|
|
49
|
+
enabled: true,
|
|
50
|
+
// source: "public/favicon.svg",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `source` is the SVG to generate from (default `public/favicon.svg`). If it does not exist, nothing is generated.
|
|
56
|
+
|
|
57
|
+
Generated files: `favicon.ico`, `favicon-16x16.png`, `favicon-32x32.png`, `apple-touch-icon.png`, `android-chrome-192x192.png`, `android-chrome-512x512.png`, `safari-pinned-tab.svg` and `site.webmanifest`.
|
|
58
|
+
|
|
59
|
+
The files are written to a cache directory under `node_modules/.cache` (not your source tree) and served from `/assets/favicon/` with `max-age=0`, so clients revalidate them (the file names are not versioned). A file that already exists in `public/assets/favicon/` is never generated or overridden, so you can supply your own version of any of them.
|
|
60
|
+
|
|
61
|
+
The `site.webmanifest` uses the `name` from your `package.json` (without scope, default `App`) as `name` and `short_name`, and the `primary` and `background` colors of the default Vuetify theme (set via `vuetify.vuetifyOptions.theme`) as `theme_color` and `background_color` (default `#ffffff`).
|
|
62
|
+
|
|
6
63
|
## Licensing
|
|
7
64
|
This project is licensed under the MIT License (MIT). See also [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CLicenseCard } from "@crystal-creations/crystal-components";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: "Molecules/CLicenseCard",
|
|
5
|
+
component: CLicenseCard,
|
|
6
|
+
args: {
|
|
7
|
+
softwareInfo: {
|
|
8
|
+
name: "Example package",
|
|
9
|
+
version: "1.2.3",
|
|
10
|
+
author: "Example Author",
|
|
11
|
+
repositoryLink: "https://github.com/example/example-package",
|
|
12
|
+
notice: `MIT License
|
|
13
|
+
|
|
14
|
+
Copyright (c) 2026 Example Author
|
|
15
|
+
|
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction.`,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Default = {};
|
|
@@ -68,8 +68,8 @@ function softwareInfo() {
|
|
|
68
68
|
// 1. Remove `[...]github.com` prefix with protocol etc., if present.
|
|
69
69
|
// 2. Remove `.git` suffix, if present.
|
|
70
70
|
const repositoryCoordinates = repositoryLink
|
|
71
|
-
.replace(
|
|
72
|
-
.replace(
|
|
71
|
+
.replace(/^(?:.*?github\.com\/)?/, "")
|
|
72
|
+
.replace(/\.git$/, "");
|
|
73
73
|
|
|
74
74
|
// 3. Prepend `https://github.com/`.
|
|
75
75
|
repositoryLink = "https://github.com/" + repositoryCoordinates;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyLicenseOverrides,
|
|
3
|
+
validateDependencyLicense,
|
|
4
|
+
type LicenseOverride,
|
|
5
|
+
} from "./license-validation.ts";
|
|
6
|
+
|
|
7
|
+
type AllowedLicense = string | Record<string, string[]>;
|
|
8
|
+
|
|
9
|
+
type LicenseModuleOptions = {
|
|
10
|
+
allowedLicenses: AllowedLicense[];
|
|
11
|
+
licenseOverrides?: Record<string, LicenseOverride>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type LicenseLogger = {
|
|
15
|
+
error(message: string): void;
|
|
16
|
+
info(message: string): void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type LicenseDependency = {
|
|
20
|
+
name: string;
|
|
21
|
+
license?: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const envKey = "ALLOWED_LICENCES";
|
|
25
|
+
|
|
26
|
+
export function createLicensePluginOptions(
|
|
27
|
+
options: LicenseModuleOptions,
|
|
28
|
+
logger: LicenseLogger,
|
|
29
|
+
) {
|
|
30
|
+
return {
|
|
31
|
+
thirdParty: {
|
|
32
|
+
includePrivate: false,
|
|
33
|
+
allow: {
|
|
34
|
+
test: (dependency: LicenseDependency) => {
|
|
35
|
+
const allowedLicensesFromEnv = process.env[envKey]
|
|
36
|
+
?.split(",")
|
|
37
|
+
.map((license) => license.trim());
|
|
38
|
+
const allowedLicenses =
|
|
39
|
+
allowedLicensesFromEnv ?? options.allowedLicenses;
|
|
40
|
+
const dependencyAllowedLicenses = allowedLicenses.flatMap(
|
|
41
|
+
(allowedLicense) => {
|
|
42
|
+
if (typeof allowedLicense === "string") return [allowedLicense];
|
|
43
|
+
|
|
44
|
+
return Object.entries(allowedLicense)
|
|
45
|
+
.filter(([, dependencies]) =>
|
|
46
|
+
dependencies.includes(dependency.name),
|
|
47
|
+
)
|
|
48
|
+
.map(([license]) => license);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
const overrides = options.licenseOverrides ?? {};
|
|
52
|
+
const validation = validateDependencyLicense(
|
|
53
|
+
dependency,
|
|
54
|
+
overrides,
|
|
55
|
+
dependencyAllowedLicenses,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!validation.license) return false;
|
|
59
|
+
|
|
60
|
+
if (!validation.isAllowed) {
|
|
61
|
+
logger.error(
|
|
62
|
+
`Dependency "${dependency.name}" has a license (${validation.license}) that is not in the dependency-specific list of allowed licenses (${dependencyAllowedLicenses.join(", ")}).`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const requiredLicenses = [...allowedLicenses, validation.license];
|
|
66
|
+
const configContent = JSON.stringify(
|
|
67
|
+
{
|
|
68
|
+
extends: "@jojomatik/nuxt-bundle",
|
|
69
|
+
licenseModule: {
|
|
70
|
+
allowedLicenses: requiredLicenses,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
undefined,
|
|
74
|
+
2,
|
|
75
|
+
);
|
|
76
|
+
const configExample =
|
|
77
|
+
"export default defineNuxtConfig(" + configContent + ");\n";
|
|
78
|
+
|
|
79
|
+
logger.info(
|
|
80
|
+
`You can specify allowed licenses in "nuxt.config.ts" environment variable:`,
|
|
81
|
+
);
|
|
82
|
+
console.log(configExample);
|
|
83
|
+
|
|
84
|
+
logger.info(
|
|
85
|
+
`You can also specify allowed licenses via "${envKey}" environment variable (ALLOWED_LICENCES=${requiredLicenses.join(",")}).`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return validation.isAllowed;
|
|
90
|
+
},
|
|
91
|
+
failOnUnlicensed: true,
|
|
92
|
+
failOnViolation: true,
|
|
93
|
+
},
|
|
94
|
+
output: {
|
|
95
|
+
file: "public/dependencies.json",
|
|
96
|
+
template(dependencies: LicenseDependency[]) {
|
|
97
|
+
return JSON.stringify(
|
|
98
|
+
applyLicenseOverrides(dependencies, options.licenseOverrides ?? {}),
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type LicenseDependency = {
|
|
2
|
+
name: string;
|
|
3
|
+
license?: string | null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type LicenseOverride =
|
|
7
|
+
string | ((license: string | undefined) => string | undefined);
|
|
8
|
+
|
|
9
|
+
function resolveLicense(
|
|
10
|
+
dependency: LicenseDependency,
|
|
11
|
+
licenseOverrides: Record<string, LicenseOverride>,
|
|
12
|
+
) {
|
|
13
|
+
const originalLicense = dependency.license ?? undefined;
|
|
14
|
+
const override = licenseOverrides[dependency.name];
|
|
15
|
+
|
|
16
|
+
if (!Object.hasOwn(licenseOverrides, dependency.name)) {
|
|
17
|
+
return originalLicense;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return typeof override === "function" ? override(originalLicense) : override;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateDependencyLicense(
|
|
24
|
+
dependency: LicenseDependency,
|
|
25
|
+
licenseOverrides: Record<string, LicenseOverride>,
|
|
26
|
+
allowedLicenses: string[],
|
|
27
|
+
) {
|
|
28
|
+
const license = resolveLicense(dependency, licenseOverrides);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
license,
|
|
32
|
+
isAllowed: Boolean(license && allowedLicenses.includes(license)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyLicenseOverrides(
|
|
37
|
+
dependencies: LicenseDependency[],
|
|
38
|
+
licenseOverrides: Record<string, LicenseOverride>,
|
|
39
|
+
) {
|
|
40
|
+
return dependencies.map((dependency) => ({
|
|
41
|
+
...dependency,
|
|
42
|
+
license: resolveLicense(dependency, licenseOverrides),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { defineNuxtModule, resolvePath, useLogger } from "nuxt/kit";
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
|
|
6
|
+
const moduleKey = "@jojomatik/nuxt-bundle/favicon-module";
|
|
7
|
+
|
|
8
|
+
const logger = useLogger(moduleKey);
|
|
9
|
+
|
|
10
|
+
/** URL path (below the app root) the files are served from. */
|
|
11
|
+
export const publicPath = "assets/favicon";
|
|
12
|
+
|
|
13
|
+
export interface FaviconModuleOptions {
|
|
14
|
+
/** Opt-in: favicons are only generated when set to `true`. */
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
/** Path of the source SVG, relative to the project root. */
|
|
17
|
+
source: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** PNG files to generate: file name and edge length in pixels. */
|
|
21
|
+
export const pngTargets = [
|
|
22
|
+
{ name: "favicon-16x16.png", size: 16 },
|
|
23
|
+
{ name: "favicon-32x32.png", size: 32 },
|
|
24
|
+
{ name: "apple-touch-icon.png", size: 180 },
|
|
25
|
+
{ name: "android-chrome-192x192.png", size: 192 },
|
|
26
|
+
{ name: "android-chrome-512x512.png", size: 512 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Removes leading and trailing slashes without regular expressions. */
|
|
30
|
+
export function trimSlashes(value: string): string {
|
|
31
|
+
let start = 0;
|
|
32
|
+
let end = value.length;
|
|
33
|
+
while (start < end && value[start] === "/") start++;
|
|
34
|
+
while (end > start && value[end - 1] === "/") end--;
|
|
35
|
+
return value.slice(start, end);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Wraps a PNG image in an ICO container. */
|
|
39
|
+
export function pngToIco(png: Buffer, size: number): Buffer {
|
|
40
|
+
const header = Buffer.alloc(22);
|
|
41
|
+
header.writeUInt16LE(0, 0); // reserved
|
|
42
|
+
header.writeUInt16LE(1, 2); // type: icon
|
|
43
|
+
header.writeUInt16LE(1, 4); // image count
|
|
44
|
+
header.writeUInt8(size >= 256 ? 0 : size, 6);
|
|
45
|
+
header.writeUInt8(size >= 256 ? 0 : size, 7);
|
|
46
|
+
header.writeUInt16LE(1, 10); // color planes
|
|
47
|
+
header.writeUInt16LE(32, 12); // bits per pixel
|
|
48
|
+
header.writeUInt32LE(png.length, 14);
|
|
49
|
+
header.writeUInt32LE(header.length, 18);
|
|
50
|
+
return Buffer.concat([header, png]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ManifestInput {
|
|
54
|
+
name?: string;
|
|
55
|
+
themeColor?: string;
|
|
56
|
+
backgroundColor?: string;
|
|
57
|
+
publicPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Builds the web manifest content (icon URLs are relative to the manifest), falling back to sensible defaults. */
|
|
61
|
+
export function createManifest({
|
|
62
|
+
name,
|
|
63
|
+
themeColor,
|
|
64
|
+
backgroundColor,
|
|
65
|
+
}: ManifestInput) {
|
|
66
|
+
const appName = name?.trim() || "App";
|
|
67
|
+
// Icons are served next to the manifest, so resolve them relative to it.
|
|
68
|
+
const base = ".";
|
|
69
|
+
return {
|
|
70
|
+
name: appName,
|
|
71
|
+
short_name: appName,
|
|
72
|
+
icons: [
|
|
73
|
+
{
|
|
74
|
+
src: `${base}/android-chrome-192x192.png`,
|
|
75
|
+
sizes: "192x192",
|
|
76
|
+
type: "image/png",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
src: `${base}/android-chrome-512x512.png`,
|
|
80
|
+
sizes: "512x512",
|
|
81
|
+
type: "image/png",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
theme_color: themeColor || "#ffffff",
|
|
85
|
+
background_color: backgroundColor || "#ffffff",
|
|
86
|
+
display: "standalone",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ThemeLike {
|
|
91
|
+
defaultTheme?: string;
|
|
92
|
+
themes?: Record<string, { colors?: Record<string, unknown> } | undefined>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Reads a theme color of the default (or first) vuetify theme. */
|
|
96
|
+
export function getThemeColor(
|
|
97
|
+
theme: ThemeLike | undefined,
|
|
98
|
+
color: string,
|
|
99
|
+
): string | undefined {
|
|
100
|
+
const themes = theme?.themes ?? {};
|
|
101
|
+
const selected =
|
|
102
|
+
themes[theme?.defaultTheme ?? "light"] ?? Object.values(themes)[0];
|
|
103
|
+
const value = selected?.colors?.[color];
|
|
104
|
+
return typeof value === "string" ? value : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generates all favicon files from the single SVG source.
|
|
109
|
+
* Returns the names of the generated files.
|
|
110
|
+
*/
|
|
111
|
+
export async function generateFavicons(
|
|
112
|
+
source: string,
|
|
113
|
+
outputDir: string,
|
|
114
|
+
skip: (name: string) => boolean = () => false,
|
|
115
|
+
): Promise<string[]> {
|
|
116
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
117
|
+
const svg = fs.readFileSync(source);
|
|
118
|
+
const generated: string[] = [];
|
|
119
|
+
|
|
120
|
+
const renderPng = (size: number) =>
|
|
121
|
+
sharp(svg, { density: 384 })
|
|
122
|
+
.resize(size, size, { fit: "contain", background: "#0000" })
|
|
123
|
+
.png()
|
|
124
|
+
.toBuffer();
|
|
125
|
+
|
|
126
|
+
const targets = pngTargets.filter(({ name }) => !skip(name));
|
|
127
|
+
const pngs = await Promise.all(targets.map(({ size }) => renderPng(size)));
|
|
128
|
+
targets.forEach(({ name }, index) => {
|
|
129
|
+
fs.writeFileSync(path.join(outputDir, name), pngs[index]);
|
|
130
|
+
generated.push(name);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!skip("favicon.ico")) {
|
|
134
|
+
const icoPng = await renderPng(48);
|
|
135
|
+
fs.writeFileSync(path.join(outputDir, "favicon.ico"), pngToIco(icoPng, 48));
|
|
136
|
+
generated.push("favicon.ico");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!skip("safari-pinned-tab.svg")) {
|
|
140
|
+
fs.copyFileSync(source, path.join(outputDir, "safari-pinned-tab.svg"));
|
|
141
|
+
generated.push("safari-pinned-tab.svg");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return generated;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default defineNuxtModule<FaviconModuleOptions>({
|
|
148
|
+
meta: {
|
|
149
|
+
name: moduleKey,
|
|
150
|
+
configKey: "faviconModule",
|
|
151
|
+
},
|
|
152
|
+
defaults: {
|
|
153
|
+
enabled: false,
|
|
154
|
+
source: "public/favicon.svg",
|
|
155
|
+
},
|
|
156
|
+
async setup(options, nuxt) {
|
|
157
|
+
if (!options.enabled) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const source = path.resolve(nuxt.options.rootDir, options.source);
|
|
162
|
+
if (!fs.existsSync(source)) {
|
|
163
|
+
logger.info(`No favicon source found at ${source}, skipping.`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Generated files live in a cache directory, never in the source tree.
|
|
168
|
+
// Nuxt's build directory can't be used, as it is cleaned during the build.
|
|
169
|
+
const outputDir = path.join(
|
|
170
|
+
nuxt.options.rootDir,
|
|
171
|
+
"node_modules/.cache/@jojomatik/nuxt-bundle/favicon",
|
|
172
|
+
);
|
|
173
|
+
const publicDir = await resolvePath(nuxt.options.dir.public);
|
|
174
|
+
const existsInSource = (name: string) =>
|
|
175
|
+
fs.existsSync(path.join(publicDir, publicPath, name));
|
|
176
|
+
|
|
177
|
+
nuxt.hook("nitro:config", async (nitroConfig) => {
|
|
178
|
+
const files = await generateFavicons(source, outputDir, existsInSource);
|
|
179
|
+
|
|
180
|
+
if (!existsInSource("site.webmanifest")) {
|
|
181
|
+
let name: string | undefined;
|
|
182
|
+
try {
|
|
183
|
+
name = JSON.parse(
|
|
184
|
+
fs.readFileSync(
|
|
185
|
+
path.join(nuxt.options.rootDir, "package.json"),
|
|
186
|
+
"utf-8",
|
|
187
|
+
),
|
|
188
|
+
).name;
|
|
189
|
+
} catch {
|
|
190
|
+
// no readable package.json, use the default name
|
|
191
|
+
}
|
|
192
|
+
if (name?.startsWith("@")) {
|
|
193
|
+
name = name.slice(name.indexOf("/") + 1);
|
|
194
|
+
}
|
|
195
|
+
const theme = (
|
|
196
|
+
nuxt.options as {
|
|
197
|
+
vuetify?: { vuetifyOptions?: { theme?: ThemeLike } };
|
|
198
|
+
}
|
|
199
|
+
).vuetify?.vuetifyOptions?.theme;
|
|
200
|
+
const manifest = createManifest({
|
|
201
|
+
name,
|
|
202
|
+
themeColor: getThemeColor(theme, "primary"),
|
|
203
|
+
backgroundColor: getThemeColor(theme, "background"),
|
|
204
|
+
publicPath,
|
|
205
|
+
});
|
|
206
|
+
fs.writeFileSync(
|
|
207
|
+
path.join(outputDir, "site.webmanifest"),
|
|
208
|
+
JSON.stringify(manifest, null, 2),
|
|
209
|
+
);
|
|
210
|
+
files.push("site.webmanifest");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
nitroConfig.publicAssets ||= [];
|
|
214
|
+
nitroConfig.publicAssets.push({
|
|
215
|
+
dir: outputDir,
|
|
216
|
+
baseURL: "/" + publicPath,
|
|
217
|
+
// Fixed file names (not versioned): always revalidate instead of caching long-term.
|
|
218
|
+
maxAge: 0,
|
|
219
|
+
});
|
|
220
|
+
logger.success(`Generated ${files.length} favicon files in ${outputDir}`);
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { addVitePlugin, defineNuxtModule, useLogger } from "nuxt/kit";
|
|
2
2
|
import license from "rollup-plugin-license";
|
|
3
|
+
import { createLicensePluginOptions } from "../lib/license-plugin-options";
|
|
3
4
|
|
|
4
5
|
const moduleKey = "@jojomatik/nuxt-bundle/license-module";
|
|
5
|
-
const envKey = "ALLOWED_LICENCES";
|
|
6
6
|
|
|
7
7
|
const logger = useLogger(moduleKey);
|
|
8
8
|
|
|
@@ -12,77 +12,6 @@ export default defineNuxtModule({
|
|
|
12
12
|
configKey: "licenseModule",
|
|
13
13
|
},
|
|
14
14
|
setup(options) {
|
|
15
|
-
addVitePlugin(
|
|
16
|
-
license({
|
|
17
|
-
thirdParty: {
|
|
18
|
-
includePrivate: false,
|
|
19
|
-
allow: {
|
|
20
|
-
test: (dependency) => {
|
|
21
|
-
// Return false for unlicensed dependencies.
|
|
22
|
-
if (!dependency.license) return false;
|
|
23
|
-
|
|
24
|
-
// Load allowed licenses from environment-variable
|
|
25
|
-
const allowedLicensesFromEnv = process.env[envKey]
|
|
26
|
-
?.split(",")
|
|
27
|
-
.map((license) => license.trim());
|
|
28
|
-
|
|
29
|
-
// Load allowed licenses from module options
|
|
30
|
-
// Allow MIT and Apache-2.0 licenses by default
|
|
31
|
-
const allowedLicenses =
|
|
32
|
-
allowedLicensesFromEnv ?? options.allowedLicenses;
|
|
33
|
-
|
|
34
|
-
const isLicenseAllowed = allowedLicenses.includes(
|
|
35
|
-
dependency.license,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
if (!isLicenseAllowed) {
|
|
39
|
-
logger.error(
|
|
40
|
-
`Dependency "${dependency.name}" has a license (${dependency.license}) that is not the list of allowed licenses (${allowedLicenses.join(", ")}).`,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
const requiredLicenses = [
|
|
44
|
-
...allowedLicenses,
|
|
45
|
-
dependency.license,
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const configContent = JSON.stringify(
|
|
49
|
-
{
|
|
50
|
-
extends: "@jojomatik/nuxt-bundle",
|
|
51
|
-
licenseModule: {
|
|
52
|
-
allowedLicenses: requiredLicenses,
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
undefined,
|
|
56
|
-
2,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const configExample =
|
|
60
|
-
"export default defineNuxtConfig(" + configContent + ");\n";
|
|
61
|
-
|
|
62
|
-
logger.info(
|
|
63
|
-
`You can specify allowed licenses in "nuxt.config.ts" environment variable:`,
|
|
64
|
-
);
|
|
65
|
-
console.log(configExample);
|
|
66
|
-
|
|
67
|
-
logger.info(
|
|
68
|
-
`You can also specify allowed licenses via "${envKey}" environment variable (ALLOWED_LICENCES=${requiredLicenses.join(",")}).`,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return isLicenseAllowed;
|
|
73
|
-
},
|
|
74
|
-
failOnUnlicensed: true,
|
|
75
|
-
failOnViolation: true,
|
|
76
|
-
},
|
|
77
|
-
output: {
|
|
78
|
-
// Output file into public directory which is included in the build output.
|
|
79
|
-
file: "public/dependencies.json",
|
|
80
|
-
template(dependencies) {
|
|
81
|
-
return JSON.stringify(dependencies);
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
}),
|
|
86
|
-
);
|
|
15
|
+
addVitePlugin(license(createLicensePluginOptions(options, logger)));
|
|
87
16
|
},
|
|
88
17
|
});
|
package/nuxt.config.ts
CHANGED
|
@@ -25,6 +25,8 @@ const isDevServer =
|
|
|
25
25
|
process.argv.some((arg) => arg.endsWith("nuxi")) &&
|
|
26
26
|
process.argv.includes("dev");
|
|
27
27
|
|
|
28
|
+
const isStorybook = process.argv.some((arg) => arg.includes("storybook"));
|
|
29
|
+
|
|
28
30
|
export default defineNuxtConfig({
|
|
29
31
|
app: {
|
|
30
32
|
pageTransition: { name: "page", mode: "out-in" },
|
|
@@ -41,6 +43,11 @@ export default defineNuxtConfig({
|
|
|
41
43
|
href: "/assets/fonts/roboto/index.css",
|
|
42
44
|
},
|
|
43
45
|
// Favicon generated with https://realfavicongenerator.net/
|
|
46
|
+
{
|
|
47
|
+
rel: "icon",
|
|
48
|
+
type: "image/vnd.microsoft.icon",
|
|
49
|
+
href: "/assets/favicon/favicon.ico",
|
|
50
|
+
},
|
|
44
51
|
{
|
|
45
52
|
rel: "icon",
|
|
46
53
|
type: "image/png",
|
|
@@ -82,7 +89,7 @@ export default defineNuxtConfig({
|
|
|
82
89
|
"vuetify-nuxt-module",
|
|
83
90
|
],
|
|
84
91
|
licenseModule: {
|
|
85
|
-
allowedLicenses: ["MIT", "Apache-2.0"],
|
|
92
|
+
allowedLicenses: ["MIT", "Apache-2.0", "ISC", "OFL-1.1"],
|
|
86
93
|
},
|
|
87
94
|
i18n: {
|
|
88
95
|
locales: getLocales(),
|
|
@@ -96,6 +103,10 @@ export default defineNuxtConfig({
|
|
|
96
103
|
storybook: {
|
|
97
104
|
enabled: !isDevServer,
|
|
98
105
|
},
|
|
106
|
+
// Storybook's Nuxt preset disables experimental.appManifest, which nuxt-skew-protection requires.
|
|
107
|
+
skewProtection: {
|
|
108
|
+
enabled: !isStorybook,
|
|
109
|
+
},
|
|
99
110
|
hooks: {
|
|
100
111
|
"nitro:build:before": () => {
|
|
101
112
|
const fontsDir = "public/assets/fonts/";
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jojomatik/nuxt-bundle",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.31",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"packageManager": "yarn@1.22.22",
|
|
5
6
|
"description": "A bundle of commonly used nuxt dependencies and configuration.",
|
|
6
7
|
"keywords": [
|
|
7
8
|
"nuxt",
|
|
@@ -11,62 +12,76 @@
|
|
|
11
12
|
"configuration"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
15
|
+
"prepare": "nuxi prepare",
|
|
14
16
|
"dev": "nuxi dev",
|
|
15
17
|
"build": "nuxi build",
|
|
16
18
|
"start": "node .output/server/index.mjs",
|
|
19
|
+
"storybook": "storybook dev -p 6006 --disable-telemetry",
|
|
20
|
+
"build-storybook": "storybook build --disable-telemetry",
|
|
17
21
|
"lint": "eslint . --max-warnings 0",
|
|
22
|
+
"test": "vitest run --coverage",
|
|
23
|
+
"test:release-notes": "node scripts/check-release-notes.ts",
|
|
18
24
|
"semantic-release": "semantic-release"
|
|
19
25
|
},
|
|
20
26
|
"files": [
|
|
21
27
|
"app",
|
|
28
|
+
"lib",
|
|
22
29
|
"locales",
|
|
23
30
|
"modules",
|
|
24
31
|
"nuxt.config.ts",
|
|
25
32
|
"package.json",
|
|
26
|
-
"
|
|
33
|
+
"public/favicon.svg",
|
|
34
|
+
"README.md",
|
|
35
|
+
"vuetify-options.ts"
|
|
27
36
|
],
|
|
28
37
|
"main": "nuxt.config.ts",
|
|
29
38
|
"dependencies": {
|
|
30
|
-
"@crystal-creations/crystal-components": "1.0.0-beta.
|
|
31
|
-
"@fontsource/roboto": "5.
|
|
39
|
+
"@crystal-creations/crystal-components": "1.0.0-beta.49",
|
|
40
|
+
"@fontsource/roboto": "5.3.0",
|
|
32
41
|
"@mdi/js": "7.4.47",
|
|
33
|
-
"@nuxt/eslint": "
|
|
34
|
-
"@nuxt/image": "2.
|
|
35
|
-
"@nuxtjs/i18n": "10.
|
|
36
|
-
"@nuxtjs/seo": "
|
|
37
|
-
"@nuxtjs/storybook": "
|
|
42
|
+
"@nuxt/eslint": "1.17.0",
|
|
43
|
+
"@nuxt/image": "2.2.0",
|
|
44
|
+
"@nuxtjs/i18n": "10.6.0",
|
|
45
|
+
"@nuxtjs/seo": "6.0.1",
|
|
46
|
+
"@nuxtjs/storybook": "10.0.3",
|
|
38
47
|
"@semantic-release/exec": "7.1.0",
|
|
39
|
-
"@semantic-release/git": "
|
|
40
|
-
"@storybook/addon-
|
|
41
|
-
"@storybook/addon-
|
|
42
|
-
"@types/node": "
|
|
43
|
-
"chromatic": "13.3
|
|
44
|
-
"conventional-changelog-conventionalcommits": "9.1
|
|
48
|
+
"@semantic-release/git": "11.0.1",
|
|
49
|
+
"@storybook/addon-docs": "10.6.5",
|
|
50
|
+
"@storybook/addon-links": "10.6.5",
|
|
51
|
+
"@types/node": "26.6.5",
|
|
52
|
+
"chromatic": "18.13.3",
|
|
53
|
+
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
45
54
|
"eslint-config-prettier": "10.1.8",
|
|
46
|
-
"eslint-plugin-prettier": "5.5.
|
|
47
|
-
"eslint-plugin-storybook": "
|
|
48
|
-
"eslint-plugin-vue": "10.
|
|
49
|
-
"prettier": "3.
|
|
50
|
-
"rollup": "4.
|
|
51
|
-
"rollup-plugin-license": "3.
|
|
52
|
-
"sass": "1.
|
|
53
|
-
"semantic-release": "25.0.
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"vuetify
|
|
55
|
+
"eslint-plugin-prettier": "5.5.7",
|
|
56
|
+
"eslint-plugin-storybook": "10.6.5",
|
|
57
|
+
"eslint-plugin-vue": "10.12.0",
|
|
58
|
+
"prettier": "3.9.10",
|
|
59
|
+
"rollup": "4.66.0",
|
|
60
|
+
"rollup-plugin-license": "3.7.1",
|
|
61
|
+
"sass": "1.106.1",
|
|
62
|
+
"semantic-release": "25.0.9",
|
|
63
|
+
"sharp": "0.34.5",
|
|
64
|
+
"storybook": "10.6.5",
|
|
65
|
+
"typescript": "6.0.3",
|
|
66
|
+
"vue-router": "5.4.0",
|
|
67
|
+
"vuetify": "3.13.7",
|
|
68
|
+
"vuetify-nuxt-module": "1.0.0-rc.6"
|
|
59
69
|
},
|
|
60
70
|
"devDependencies": {
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
71
|
+
"@vitejs/plugin-vue": "6.0.10",
|
|
72
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
73
|
+
"@vue/test-utils": "2.5.1",
|
|
74
|
+
"eslint": "10.13.0",
|
|
75
|
+
"happy-dom": "21.0.4",
|
|
76
|
+
"nuxt": "4.6.3",
|
|
77
|
+
"vite": "8.3.5",
|
|
78
|
+
"vitest": "5.0.5",
|
|
79
|
+
"vue": "3.5.44"
|
|
65
80
|
},
|
|
66
81
|
"peerDependencies": {
|
|
67
|
-
"eslint": "^9.39.2",
|
|
68
|
-
"nuxt": "^4.
|
|
69
|
-
"vite": "^7.3.0",
|
|
82
|
+
"eslint": "^9.39.2 || ^10.0.0",
|
|
83
|
+
"nuxt": "^4.6.1",
|
|
84
|
+
"vite": "^7.3.0 || ^8.0.0",
|
|
70
85
|
"vue": "^3.5.26"
|
|
71
86
|
},
|
|
72
87
|
"author": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#Unbenannter_Verlauf_2);}.cls-3{fill:url(#Unbenannter_Verlauf_3);}</style><linearGradient id="Unbenannter_Verlauf_2" x1="191.53" y1="124.12" x2="445.95" y2="378.54" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#004c6d"/><stop offset="1" stop-color="#037a7a"/></linearGradient><linearGradient id="Unbenannter_Verlauf_3" x1="133.22" y1="194.26" x2="382.3" y2="443.34" gradientUnits="userSpaceOnUse"><stop offset="0.01" stop-color="#004c6d"/><stop offset="1" stop-color="#037a7a"/></linearGradient></defs><rect class="cls-1" width="512" height="512"/><path class="cls-2" d="M204.21,371.81l33.08-33.09c19.46,15.57,32.76,33,52.69,33,30.49,0,46.23-16.54,46.23-63V90.11H394V313.69c0,59.58-29.29,108.2-97.24,108.2C251.12,421.89,231.45,402.94,204.21,371.81Z"/><polygon class="cls-3" points="209.4 311.75 176.03 345.11 68 237.07 176.03 129.04 209.4 162.4 134.73 237.07 209.4 311.75"/></svg>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { VOptions } from "vuetify-nuxt-module";
|
|
2
|
+
|
|
3
|
+
const vuetifyOptions: VOptions = {
|
|
4
|
+
theme: {
|
|
5
|
+
defaultTheme: "light",
|
|
6
|
+
themes: {
|
|
7
|
+
light: {
|
|
8
|
+
dark: false,
|
|
9
|
+
colors: {
|
|
10
|
+
background: "#EDEDED",
|
|
11
|
+
surface: "#FCFCFC",
|
|
12
|
+
"surface-variant": "#424242",
|
|
13
|
+
"on-surface-variant": "#EEEEEE",
|
|
14
|
+
primary: "#004c6d",
|
|
15
|
+
"primary-darken-1": "#3700B3",
|
|
16
|
+
secondary: "#037a7a",
|
|
17
|
+
"secondary-darken-1": "#018786",
|
|
18
|
+
error: "#B00020",
|
|
19
|
+
info: "#2196F3",
|
|
20
|
+
success: "#4CAF50",
|
|
21
|
+
warning: "#FB8C00",
|
|
22
|
+
},
|
|
23
|
+
variables: {},
|
|
24
|
+
},
|
|
25
|
+
dark: {
|
|
26
|
+
dark: true,
|
|
27
|
+
colors: {
|
|
28
|
+
background: "#121212",
|
|
29
|
+
surface: "#212121",
|
|
30
|
+
"surface-variant": "#BDBDBD",
|
|
31
|
+
"on-surface-variant": "#424242",
|
|
32
|
+
primary: "#0074a5",
|
|
33
|
+
secondary: "#039e9e",
|
|
34
|
+
error: "#CF6679",
|
|
35
|
+
info: "#2196F3",
|
|
36
|
+
success: "#4CAF50",
|
|
37
|
+
warning: "#FB8C00",
|
|
38
|
+
},
|
|
39
|
+
variables: {},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
icons: {
|
|
44
|
+
defaultSet: "mdi-svg",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default vuetifyOptions;
|