vite-plugin-spiral 0.1.0

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 ADDED
@@ -0,0 +1,115 @@
1
+ # vite-plugin-spiral
2
+
3
+ Vite integration for Spiral applications. It configures backend-friendly manifests, hot files, full page refresh, SRI metadata, SSR output, typed asset directories, optional asset glob processing, and optional self-hosted fonts.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -D vite-plugin-spiral
9
+ ```
10
+
11
+ Install `fontaine` only when using metric-optimized font fallbacks:
12
+
13
+ ```bash
14
+ npm install -D fontaine
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```ts
20
+ import { defineConfig } from 'vite'
21
+ import spiral from 'vite-plugin-spiral'
22
+
23
+ export default defineConfig({
24
+ plugins: [
25
+ spiral({
26
+ input: [
27
+ 'app/resources/css/fonts.css',
28
+ 'app/resources/css/app.css',
29
+ 'app/resources/js/app.ts',
30
+ ],
31
+ refresh: true,
32
+ integrity: true,
33
+ }),
34
+ ],
35
+ })
36
+ ```
37
+
38
+ Use application scripts:
39
+
40
+ ```bash
41
+ npm run dev
42
+ npm run build
43
+ ```
44
+
45
+ ## Options
46
+
47
+ - `input`: entry point or entry points. Shorthand is supported: `spiral('app/resources/js/app.ts')`.
48
+ - `publicDirectory`: defaults to `public`.
49
+ - `buildDirectory`: defaults to `build`.
50
+ - `hotFile`: defaults to `runtime/vite.hot`.
51
+ - `ssr`: SSR entry point; `ssrOutputDirectory` defaults to `runtime/vite-ssr`.
52
+ - `refresh`: `true`, `false`, paths, or `{ paths, config }` for `vite-plugin-full-reload`.
53
+ - `integrity`: `true` uses `sha384`; `sha256`, `sha384`, and `sha512` are supported.
54
+ - `assets`: glob or globs emitted as versioned build assets.
55
+ - `fonts`: optional local, Google, Bunny, or Fontsource font definitions.
56
+ - `output`: custom `jsDirectory`, `cssDirectory`, `imageDirectory`, `fontDirectory`, and `assetDirectory`.
57
+ - `detectTls`: `true`, a host string, `null`, or `false` for Herd / Valet TLS detection.
58
+ - `valetTls`: deprecated alias for `detectTls`; `detectTls` wins when both are set.
59
+
60
+ The plugin adds aliases for `@` -> `app/resources/js` and `@resources` -> `app/resources`. User aliases override defaults.
61
+ Vite 8 `build.rolldownOptions.input` is supported, with `build.rollupOptions.input` accepted as a migration fallback.
62
+
63
+ ## Environment
64
+
65
+ - `SPIRAL_VITE_ASSET_URL`, fallback `ASSET_URL`, prefixes production asset URLs.
66
+ - `SPIRAL_APP_URL`, fallback `APP_URL`, is used for CORS and dev-server output.
67
+ - `SPIRAL_VITE_PORT`, fallback `VITE_PORT`, sets the dev server port.
68
+ - `SPIRAL_VITE_ALLOWED_ORIGINS` adds comma-separated CORS origins.
69
+ - `SPIRAL_VITE_DEV_SERVER_KEY` and `SPIRAL_VITE_DEV_SERVER_CERT` enable HTTPS.
70
+ - `SPIRAL_VITE_BYPASS_ENV_CHECK=1` allows running dev server in CI or production envs.
71
+
72
+ ## Fonts
73
+
74
+ Fonts are opt-in and do not replace standalone CSS entries.
75
+
76
+ ```ts
77
+ import spiral from 'vite-plugin-spiral'
78
+ import { local, google, fontsource } from 'vite-plugin-spiral/fonts'
79
+
80
+ spiral({
81
+ input: ['app/resources/css/fonts.css', 'app/resources/css/app.css', 'app/resources/js/app.ts'],
82
+ fonts: [
83
+ local('Chocolate Classical Sans', {
84
+ variants: [
85
+ {
86
+ src: 'app/resources/fonts/ChocolateClassicalSans-Regular.woff2',
87
+ weight: 400,
88
+ },
89
+ ],
90
+ fallbacks: ['Noto Sans TC', 'PingFang TC', 'Microsoft JhengHei'],
91
+ optimizedFallbacks: true,
92
+ }),
93
+ google('Montserrat', {
94
+ weights: [300, 400, 500, 600, 700],
95
+ subsets: ['latin'],
96
+ }),
97
+ fontsource('Inter', {
98
+ weights: [400, 600],
99
+ }),
100
+ ],
101
+ })
102
+ ```
103
+
104
+ Builds emit `fonts-manifest.json`; dev writes `runtime/fonts-manifest.dev.json`. `optimizedFallbacks: true` generates metric-adjusted fallback `@font-face` rules through the optional `fontaine` peer dependency and fails loudly when `fontaine` is missing.
105
+
106
+ ## Cleaning Orphaned Assets
107
+
108
+ After repeated production builds, remove files no longer referenced by manifests:
109
+
110
+ ```bash
111
+ npx spiral-vite-clean --dry-run --verbose
112
+ npx spiral-vite-clean
113
+ ```
114
+
115
+ The command scans `public/build/manifest.json`, `fonts-manifest.json`, and `ssr-manifest.json` when present. It only deletes files inside the configured build directory.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ interface CleanOptions {
3
+ cwd?: string;
4
+ publicDirectory?: string;
5
+ buildDirectory?: string;
6
+ manifest?: string;
7
+ fontsManifest?: string;
8
+ dryRun?: boolean;
9
+ verbose?: boolean;
10
+ }
11
+ interface CleanResult {
12
+ buildRoot: string;
13
+ deleted: string[];
14
+ kept: string[];
15
+ manifests: string[];
16
+ }
17
+ declare function cleanOrphanedAssets(options?: CleanOptions): Promise<CleanResult>;
18
+ declare function runCleanAssetsCli(argv?: string[], cwd?: string): Promise<number>;
19
+
20
+ export { type CleanOptions, type CleanResult, cleanOrphanedAssets, runCleanAssetsCli };
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/clean.ts
4
+ import { promises as fs } from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ async function cleanOrphanedAssets(options = {}) {
8
+ const cwd = options.cwd ?? process.cwd();
9
+ const publicDirectory = trimSlashes(options.publicDirectory ?? "public");
10
+ const buildDirectory = trimSlashes(options.buildDirectory ?? "build");
11
+ const manifest = options.manifest ?? "manifest.json";
12
+ const fontsManifest = options.fontsManifest ?? "fonts-manifest.json";
13
+ const buildRoot = path.resolve(cwd, publicDirectory, buildDirectory);
14
+ const referenced = /* @__PURE__ */ new Set();
15
+ const manifests = [];
16
+ if (!await isDirectory(buildRoot)) {
17
+ return { buildRoot, deleted: [], kept: [], manifests: [] };
18
+ }
19
+ for (const name of [manifest, fontsManifest, "ssr-manifest.json"]) {
20
+ const relative = normalizeRelative(name);
21
+ if (relative === null) {
22
+ throw new Error(`vite-plugin-spiral: Manifest path "${name}" must be relative.`);
23
+ }
24
+ const file = path.join(buildRoot, relative);
25
+ if (!await isFile(file)) {
26
+ continue;
27
+ }
28
+ referenced.add(relative);
29
+ manifests.push(relative);
30
+ collectReferences(JSON.parse(await fs.readFile(file, "utf8")), referenced, buildDirectory);
31
+ }
32
+ if (manifests.length === 0) {
33
+ throw new Error(`vite-plugin-spiral: No manifest files were found in "${buildRoot}". Refusing to clean without a live asset list.`);
34
+ }
35
+ const files = await listFiles(buildRoot);
36
+ const deleted = [];
37
+ const kept = [];
38
+ for (const file of files) {
39
+ if (referenced.has(file) || path.basename(file) === "vite.hot") {
40
+ kept.push(file);
41
+ continue;
42
+ }
43
+ deleted.push(file);
44
+ if (!options.dryRun) {
45
+ await fs.rm(safeJoin(buildRoot, file), { force: true });
46
+ }
47
+ }
48
+ if (!options.dryRun) {
49
+ await removeEmptyDirectories(buildRoot);
50
+ }
51
+ return { buildRoot, deleted, kept, manifests };
52
+ }
53
+ async function runCleanAssetsCli(argv = process.argv.slice(2), cwd = process.cwd()) {
54
+ const options = parseArgs(argv);
55
+ const result = await cleanOrphanedAssets({ ...options, cwd });
56
+ if (options.verbose || options.dryRun) {
57
+ const action = options.dryRun ? "Would delete" : "Deleted";
58
+ for (const file of result.deleted) {
59
+ console.log(`${action}: ${file}`);
60
+ }
61
+ }
62
+ if (options.verbose) {
63
+ console.log(`Build directory: ${result.buildRoot}`);
64
+ console.log(`Manifests: ${result.manifests.length === 0 ? "(none)" : result.manifests.join(", ")}`);
65
+ console.log(`Kept: ${result.kept.length}`);
66
+ console.log(`${options.dryRun ? "Matched orphans" : "Deleted orphans"}: ${result.deleted.length}`);
67
+ }
68
+ return 0;
69
+ }
70
+ function parseArgs(argv) {
71
+ const options = {};
72
+ for (let index = 0; index < argv.length; index++) {
73
+ const arg = argv[index];
74
+ if (arg === "--dry-run") {
75
+ options.dryRun = true;
76
+ continue;
77
+ }
78
+ if (arg === "--verbose") {
79
+ options.verbose = true;
80
+ continue;
81
+ }
82
+ const [name, inlineValue] = arg.split("=", 2);
83
+ if (!name.startsWith("--")) {
84
+ throw new Error(`vite-plugin-spiral: Unknown argument "${arg}".`);
85
+ }
86
+ const value = inlineValue ?? argv[++index];
87
+ if (value === void 0 || value.startsWith("--")) {
88
+ throw new Error(`vite-plugin-spiral: Option "${name}" requires a value.`);
89
+ }
90
+ if (name === "--public-directory") {
91
+ options.publicDirectory = value;
92
+ } else if (name === "--build-directory") {
93
+ options.buildDirectory = value;
94
+ } else if (name === "--manifest") {
95
+ options.manifest = value;
96
+ } else if (name === "--fonts-manifest") {
97
+ options.fontsManifest = value;
98
+ } else {
99
+ throw new Error(`vite-plugin-spiral: Unknown argument "${arg}".`);
100
+ }
101
+ }
102
+ return options;
103
+ }
104
+ function collectReferences(value, references, buildDirectory) {
105
+ if (typeof value === "string") {
106
+ const reference = normalizeManifestReference(value, buildDirectory);
107
+ if (reference !== null) {
108
+ references.add(reference);
109
+ }
110
+ return;
111
+ }
112
+ if (Array.isArray(value)) {
113
+ for (const item of value) {
114
+ collectReferences(item, references, buildDirectory);
115
+ }
116
+ return;
117
+ }
118
+ if (typeof value === "object" && value !== null) {
119
+ for (const item of Object.values(value)) {
120
+ collectReferences(item, references, buildDirectory);
121
+ }
122
+ }
123
+ }
124
+ function normalizeManifestReference(value, buildDirectory) {
125
+ if (value === "" || /^[a-z][a-z\d+.-]*:/i.test(value) || value.startsWith("//")) {
126
+ return null;
127
+ }
128
+ const cleaned = value.split("#", 1)[0].split("?", 1)[0];
129
+ let relative = cleaned.replace(/^\/+/, "").replace(/\\/g, "/");
130
+ const buildPrefix = `${trimSlashes(buildDirectory)}/`;
131
+ if (relative.startsWith(buildPrefix)) {
132
+ relative = relative.slice(buildPrefix.length);
133
+ }
134
+ return normalizeRelative(relative);
135
+ }
136
+ async function listFiles(root, prefix = "") {
137
+ const entries = await fs.readdir(path.join(root, prefix), { withFileTypes: true });
138
+ const files = await Promise.all(entries.map(async (entry) => {
139
+ const relative = path.posix.join(prefix, entry.name);
140
+ if (entry.isDirectory()) {
141
+ return listFiles(root, relative);
142
+ }
143
+ return [relative];
144
+ }));
145
+ return files.flat().sort();
146
+ }
147
+ async function removeEmptyDirectories(root, prefix = "") {
148
+ const absolute = path.join(root, prefix);
149
+ const entries = await fs.readdir(absolute, { withFileTypes: true });
150
+ for (const entry of entries) {
151
+ if (entry.isDirectory()) {
152
+ await removeEmptyDirectories(root, path.posix.join(prefix, entry.name));
153
+ }
154
+ }
155
+ if (prefix !== "" && (await fs.readdir(absolute)).length === 0) {
156
+ await fs.rmdir(absolute);
157
+ }
158
+ }
159
+ async function isFile(file) {
160
+ try {
161
+ return (await fs.stat(file)).isFile();
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+ async function isDirectory(directory) {
167
+ try {
168
+ return (await fs.stat(directory)).isDirectory();
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+ function safeJoin(root, relative) {
174
+ const resolved = path.resolve(root, relative);
175
+ const normalizedRoot = path.resolve(root);
176
+ if (resolved !== normalizedRoot && !resolved.startsWith(`${normalizedRoot}${path.sep}`)) {
177
+ throw new Error(`vite-plugin-spiral: Refusing to delete path outside build directory: ${relative}`);
178
+ }
179
+ return resolved;
180
+ }
181
+ function normalizeRelative(value) {
182
+ const relative = value.replace(/\\/g, "/").replace(/^\/+/, "");
183
+ if (relative === "" || relative.split("/").includes("..")) {
184
+ return null;
185
+ }
186
+ return relative;
187
+ }
188
+ function trimSlashes(value) {
189
+ return value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
190
+ }
191
+ var currentFile = fileURLToPath(import.meta.url);
192
+ if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) {
193
+ runCleanAssetsCli().then((code) => {
194
+ process.exitCode = code;
195
+ }).catch((error) => {
196
+ console.error(error.message);
197
+ process.exitCode = 1;
198
+ });
199
+ }
200
+ export {
201
+ cleanOrphanedAssets,
202
+ runCleanAssetsCli
203
+ };
@@ -0,0 +1,49 @@
1
+ // src/fonts/providers.ts
2
+ function google(family, options) {
3
+ return buildFontDefinition(family, "google", options);
4
+ }
5
+ function bunny(family, options) {
6
+ return buildFontDefinition(family, "bunny", options);
7
+ }
8
+ function fontsource(family, options) {
9
+ return buildFontDefinition(family, "fontsource", options, {
10
+ _fontsource: { package: options?.package }
11
+ });
12
+ }
13
+ function local(family, options) {
14
+ const localOptions = "src" in options && options.src !== void 0 ? { src: options.src } : { variants: options.variants };
15
+ return buildFontDefinition(family, "local", options, {
16
+ weights: [],
17
+ styles: [],
18
+ subsets: [],
19
+ _local: localOptions
20
+ });
21
+ }
22
+ function buildFontDefinition(family, provider, options, extra = {}) {
23
+ const alias = options?.alias ?? familyToSlug(family);
24
+ return {
25
+ family,
26
+ alias,
27
+ provider,
28
+ variable: options?.variable ?? `--font-${alias}`,
29
+ weights: options?.weights ? [...options.weights] : [400],
30
+ styles: options?.styles ?? ["normal"],
31
+ subsets: options?.subsets ?? ["latin"],
32
+ display: options?.display ?? "swap",
33
+ preload: options?.preload ?? true,
34
+ fallbacks: options?.fallbacks ?? [],
35
+ optimizedFallbacks: options?.optimizedFallbacks ?? false,
36
+ ...extra
37
+ };
38
+ }
39
+ function familyToSlug(family) {
40
+ return family.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
41
+ }
42
+
43
+ export {
44
+ google,
45
+ bunny,
46
+ fontsource,
47
+ local,
48
+ familyToSlug
49
+ };
@@ -0,0 +1,9 @@
1
+ import { j as FontWeight, R as RemoteFontOptions, F as FontDefinition, k as FontsourceFontOptions, L as LocalFontOptions } from '../types-B2Q2BHEj.js';
2
+ export { B as BaseFontOptions, a as FontDisplay, b as FontFormat, c as FontManifest, d as FontManifestFamily, e as FontManifestPreload, f as FontManifestVariant, g as FontManifestVariantFile, h as FontProviderType, i as FontStyle, l as LocalVariantDefinition, P as ParsedFontFace, m as ParsedFontSrc, n as PreloadSelector, o as RemoteFontStyle, p as ResolvedFontFamily, q as ResolvedFontFile, r as ResolvedFontVariant } from '../types-B2Q2BHEj.js';
3
+
4
+ declare function google<const W extends FontWeight = FontWeight>(family: string, options?: RemoteFontOptions<W>): FontDefinition;
5
+ declare function bunny<const W extends FontWeight = FontWeight>(family: string, options?: RemoteFontOptions<W>): FontDefinition;
6
+ declare function fontsource<const W extends FontWeight = FontWeight>(family: string, options?: FontsourceFontOptions<W>): FontDefinition;
7
+ declare function local(family: string, options: LocalFontOptions): FontDefinition;
8
+
9
+ export { FontDefinition, FontWeight, FontsourceFontOptions, LocalFontOptions, RemoteFontOptions, bunny, fontsource, google, local };
@@ -0,0 +1,12 @@
1
+ import {
2
+ bunny,
3
+ fontsource,
4
+ google,
5
+ local
6
+ } from "../chunk-VHEBKNM2.js";
7
+ export {
8
+ bunny,
9
+ fontsource,
10
+ google,
11
+ local
12
+ };
@@ -0,0 +1,44 @@
1
+ import { PluginOption } from 'vite';
2
+ import { Config } from 'vite-plugin-full-reload';
3
+ import { F as FontDefinition } from './types-B2Q2BHEj.js';
4
+
5
+ type SpiralInput = string | string[] | Record<string, string>;
6
+ type SpiralPluginConfig = SpiralInput | SpiralPluginOptions;
7
+ type SpiralRefreshConfig = {
8
+ paths: string[];
9
+ config?: Config;
10
+ };
11
+ type SpiralRefreshOption = boolean | string | string[] | SpiralRefreshConfig | SpiralRefreshConfig[];
12
+ type SpiralIntegrityAlgorithm = 'sha256' | 'sha384' | 'sha512';
13
+ interface SpiralOutputOptions {
14
+ jsDirectory?: string;
15
+ cssDirectory?: string;
16
+ imageDirectory?: string;
17
+ fontDirectory?: string;
18
+ assetDirectory?: string;
19
+ }
20
+ interface SpiralPluginOptions {
21
+ input: SpiralInput;
22
+ publicDirectory?: string;
23
+ buildDirectory?: string;
24
+ hotFile?: string;
25
+ refresh?: SpiralRefreshOption;
26
+ ssr?: SpiralInput;
27
+ ssrOutputDirectory?: string;
28
+ integrity?: boolean | SpiralIntegrityAlgorithm;
29
+ assetUrl?: string;
30
+ detectTls?: string | boolean | null;
31
+ /** @deprecated Use detectTls instead. */
32
+ valetTls?: string | boolean | null;
33
+ assets?: string | string[];
34
+ fonts?: FontDefinition[];
35
+ transformOnServe?: (code: string, devServerUrl: string) => string;
36
+ server?: {
37
+ origin?: string;
38
+ corsOrigin?: string | string[];
39
+ };
40
+ output?: SpiralOutputOptions;
41
+ }
42
+ declare function spiral(config: SpiralPluginConfig): PluginOption[];
43
+
44
+ export { type SpiralInput, type SpiralIntegrityAlgorithm, type SpiralOutputOptions, type SpiralPluginConfig, type SpiralPluginOptions, type SpiralRefreshConfig, type SpiralRefreshOption, spiral as default, spiral };