@greendrake/vite-preset 0.1.0 → 0.3.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 +1 -6
- package/package.json +3 -6
- package/src/apiTarget.ts +5 -3
- package/src/html.ts +2 -22
- package/src/main.ts +20 -21
- package/src/sourceDeps.ts +6 -3
- package/src/staticRoots.ts +1 -1
package/README.md
CHANGED
|
@@ -34,13 +34,12 @@ export default defineConfig(
|
|
|
34
34
|
| `scssLoadPaths` | Extra `@use` resolution roots for Sass (e.g. the app's `src/scss`). |
|
|
35
35
|
| `port` | `'env'` (default): `VITE_PORT` is required whenever a server starts. A number pins that port. `false` disables pinning, for an app served on any free port. |
|
|
36
36
|
| `htmlPlaceholders` | Literal substitutions applied to `index.html` before Vite processes it (see `htmlPlaceholders` below). |
|
|
37
|
-
| `htmlMinify` | Minify `index.html`, inline scripts and styles included, in production builds. |
|
|
38
37
|
| `staticRoots` | Directories outside the app served at public URL prefixes (see `staticRoots` below). |
|
|
39
38
|
| `overrides` | A `UserConfig` deep-merged last — the escape hatch for `server.proxy`, extra plugins, build tweaks. |
|
|
40
39
|
|
|
41
40
|
What the preset sets:
|
|
42
41
|
|
|
43
|
-
- plugins: `@vitejs/plugin-vue` and the HMR circular-import guard, plus the `staticRoots
|
|
42
|
+
- plugins: `@vitejs/plugin-vue` and the HMR circular-import guard, plus the `staticRoots` and `htmlPlaceholders` plugins when their options are given;
|
|
44
43
|
- `server.port`/`preview.port` from `VITE_PORT` with `strictPort`, for `vite dev` and `vite preview` only — builds run in deploy contexts that never define a dev port;
|
|
45
44
|
- `server.fs.allow`: the parent of the app root (a workspace), and every static root's directory;
|
|
46
45
|
- `resolve.alias`: `@` → `<dirname>/src`; `resolve.dedupe`: `vue`, `pinia`, `vue-router` — each must also be a direct dependency of the app, or Vite fails to resolve it;
|
|
@@ -88,10 +87,6 @@ import { loadAppEnv, requirePort, envBool, deriveImageBaseUrl, apiTargetEnv, bui
|
|
|
88
87
|
|
|
89
88
|
Substitutes build-time values into `index.html` before Vite processes it — a generated inline script, a build hash, PWA metadata. Keys are matched literally (convention: `__NAME__`); an empty map is an error. `vueSpa`'s `htmlPlaceholders` option installs it.
|
|
90
89
|
|
|
91
|
-
### `htmlMinify()`
|
|
92
|
-
|
|
93
|
-
Production-only: collapses whitespace, strips comments, minifies inline JS and CSS in `index.html`. Dev keeps the source readable. Installed by the `htmlMinify` option.
|
|
94
|
-
|
|
95
90
|
### `staticRoots(roots)` and `staticRootFiles(root)`
|
|
96
91
|
|
|
97
92
|
Some assets belong to the product rather than to the frontend — a locale catalogue a backend renders from too, prose a content page shows. A `StaticRoot` is `{ dir, urlPath, transform? }`: an absolute directory, the URL prefix it appears under (e.g. `/i18n`), and an optional per-file rewrite applied on the way out. The plugin serves the root by middleware in dev and copies it into `dist/` at build, so the two modes publish the same bytes; `.yaml`/`.yml` and `.json` get their content types, anything else is `text/plain`. Requests cannot climb out of the root. `staticRootFiles(root)` lists what a root publishes as URL paths, for a service worker's precache list. Installed by the `staticRoots` option.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greendrake/vite-preset",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Vite config factory and build-env helpers for Vue 3 single-page apps: plugin set, strict dev port, alias, dedupe and source-package pre-bundling rules",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eugene Greendrake <eugene@greendrake.nz>",
|
|
@@ -38,17 +38,14 @@
|
|
|
38
38
|
},
|
|
39
39
|
"prettier": "@greendrake/dev-config/prettier",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@vitejs/plugin-vue": "^6.0.7",
|
|
43
|
-
"html-minifier-terser": "^7.2.0"
|
|
41
|
+
"@vitejs/plugin-vue": "^6.0.7"
|
|
44
42
|
},
|
|
45
43
|
"peerDependencies": {
|
|
46
44
|
"vite": ">=8"
|
|
47
45
|
},
|
|
48
46
|
"devDependencies": {
|
|
49
|
-
"@greendrake/dev-config": "0.
|
|
47
|
+
"@greendrake/dev-config": "0.3.0",
|
|
50
48
|
"@types/bun": "^1.3.5",
|
|
51
|
-
"@types/html-minifier-terser": "^7.0.2",
|
|
52
49
|
"@types/node": "^24.0.0",
|
|
53
50
|
"typescript": "6.0.3",
|
|
54
51
|
"vite": "^8.2.1"
|
package/src/apiTarget.ts
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
export interface ApiTargetEnv {
|
|
18
18
|
// True under `vite dev` (command === 'serve'); false in production builds.
|
|
19
19
|
dev: boolean
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// Undefined as readily as absent: each is an environment variable, which
|
|
21
|
+
// is either set or not.
|
|
22
|
+
apiPort?: string | undefined
|
|
23
|
+
publicFeHost?: string | undefined
|
|
24
|
+
publicApiHost?: string | undefined
|
|
23
25
|
// Endpoint paths appended to the resolved origin (defaults /v1/api and
|
|
24
26
|
// /v1/ws). A backend that routes POST at the host root sets apiPath: ''.
|
|
25
27
|
apiPath?: string
|
package/src/html.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { minify } from 'html-minifier-terser'
|
|
2
1
|
import type { Plugin } from 'vite'
|
|
3
2
|
|
|
4
3
|
// Substitutes build-time values into index.html before Vite processes it —
|
|
@@ -14,27 +13,8 @@ export const htmlPlaceholders = (map: Record<string, string>): Plugin => {
|
|
|
14
13
|
name: 'greendrake-html-placeholders',
|
|
15
14
|
transformIndexHtml: {
|
|
16
15
|
order: 'pre',
|
|
17
|
-
|
|
16
|
+
// The pattern is built from the map's own keys, so every match is one.
|
|
17
|
+
handler: html => html.replace(pattern, match => map[match]!)
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
// Production-only HTML minification — strips comments and collapses
|
|
23
|
-
// whitespace in index.html, and runs inline scripts/styles through
|
|
24
|
-
// terser/CSS minification. Dev keeps the source readable.
|
|
25
|
-
export const htmlMinify = (): Plugin => ({
|
|
26
|
-
name: 'greendrake-html-minify',
|
|
27
|
-
apply: 'build',
|
|
28
|
-
transformIndexHtml: {
|
|
29
|
-
order: 'post',
|
|
30
|
-
handler: html =>
|
|
31
|
-
minify(html, {
|
|
32
|
-
collapseWhitespace: true,
|
|
33
|
-
removeComments: true,
|
|
34
|
-
minifyJS: true,
|
|
35
|
-
minifyCSS: true,
|
|
36
|
-
useShortDoctype: true,
|
|
37
|
-
removeRedundantAttributes: true
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
})
|
package/src/main.ts
CHANGED
|
@@ -8,11 +8,11 @@ export { envBool, deriveImageBaseUrl, loadAppEnv, requirePort, apiTargetEnv, bui
|
|
|
8
8
|
// @greendrake/dash/vite) speak this package's vite types: a second vite
|
|
9
9
|
// copy resolved downstream is a structurally unrelated set of the same names.
|
|
10
10
|
export type { ConfigEnv, UserConfig, UserConfigFnObject } from 'vite'
|
|
11
|
-
export { htmlPlaceholders
|
|
11
|
+
export { htmlPlaceholders } from './html'
|
|
12
12
|
export { resolveApiTarget, browserRuntime, type ApiTarget, type ApiTargetEnv, type ApiTargetRuntime } from './apiTarget'
|
|
13
13
|
export { staticRoots, staticRootFiles, type StaticRoot } from './staticRoots'
|
|
14
14
|
import { requirePort } from './env'
|
|
15
|
-
import { htmlPlaceholders
|
|
15
|
+
import { htmlPlaceholders } from './html'
|
|
16
16
|
import { hmrCircularImportGuard } from './hmr'
|
|
17
17
|
import { nestedDependencies, type DeclaredDependencies } from './sourceDeps'
|
|
18
18
|
import { staticRoots as staticRootsPlugin, type StaticRoot } from './staticRoots'
|
|
@@ -23,7 +23,7 @@ export interface VueSpaOptions {
|
|
|
23
23
|
// Compile-time globals; values are serialized automatically (strings become
|
|
24
24
|
// string literals, booleans become true/false, objects become object
|
|
25
25
|
// literals — e.g. the ApiTargetEnv carried by __API_TARGET_ENV__).
|
|
26
|
-
define?: Record<string, string | number | boolean | object | null | undefined>
|
|
26
|
+
define?: Record<string, string | number | boolean | object | null | undefined> | undefined
|
|
27
27
|
// Extra scss @use resolution roots (e.g. the app's src/scss).
|
|
28
28
|
scssLoadPaths?: string[]
|
|
29
29
|
// 'env' (default): the strict VITE_PORT convention, required when the dev
|
|
@@ -34,8 +34,6 @@ export interface VueSpaOptions {
|
|
|
34
34
|
// Build-time placeholder substitution in index.html (see the
|
|
35
35
|
// htmlPlaceholders plugin) — e.g. injecting an inline bootstrap script.
|
|
36
36
|
htmlPlaceholders?: Record<string, string>
|
|
37
|
-
// Minify index.html (incl. inline JS/CSS) in production builds.
|
|
38
|
-
htmlMinify?: boolean
|
|
39
37
|
// Directories outside the app served at a public URL prefix — product
|
|
40
38
|
// assets both stacks own (locale catalogues, content prose). Served by
|
|
41
39
|
// middleware in dev and copied into dist/ at build, so the two modes
|
|
@@ -48,10 +46,10 @@ export interface VueSpaOptions {
|
|
|
48
46
|
// source-shipped by construction — an app with source packages of its own
|
|
49
47
|
// names them here, and must name the @greendrake scope again if it uses
|
|
50
48
|
// any.
|
|
51
|
-
sourcePackages?: string[]
|
|
49
|
+
sourcePackages?: string[] | undefined
|
|
52
50
|
// Deep-merged last over the preset — the escape hatch for anything
|
|
53
51
|
// app-specific (server.proxy, extra plugins, build tweaks).
|
|
54
|
-
overrides?: UserConfig
|
|
52
|
+
overrides?: UserConfig | undefined
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
// The libraries the app and every component package it renders must share one
|
|
@@ -89,11 +87,11 @@ export const vueSpa =
|
|
|
89
87
|
// none (and never serves) must not be made to produce one.
|
|
90
88
|
const portPin = command === 'serve' && options.port !== false ? { port: typeof options.port === 'number' ? options.port : requirePort(), strictPort: true } : undefined
|
|
91
89
|
const base: UserConfig = {
|
|
92
|
-
|
|
93
|
-
plugins: [vue(), hmrCircularImportGuard(), ...(options.staticRoots?.length ? [staticRootsPlugin(options.staticRoots)] : []), ...(options.htmlPlaceholders ? [htmlPlaceholders(options.htmlPlaceholders)] : [])
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
...(options.define && { define: Object.fromEntries(Object.entries(options.define).map(([key, value]) => [key, JSON.stringify(value)])) }),
|
|
91
|
+
plugins: [vue(), hmrCircularImportGuard(), ...(options.staticRoots?.length ? [staticRootsPlugin(options.staticRoots)] : []), ...(options.htmlPlaceholders ? [htmlPlaceholders(options.htmlPlaceholders)] : [])],
|
|
92
|
+
...(command === 'serve'
|
|
93
|
+
? {
|
|
94
|
+
server: {
|
|
97
95
|
...portPin,
|
|
98
96
|
// '..' reaches the workspace an app sits directly
|
|
99
97
|
// inside. Anything the dev server must read from
|
|
@@ -104,10 +102,11 @@ export const vueSpa =
|
|
|
104
102
|
// refused.
|
|
105
103
|
fs: { allow: ['..', ...(options.staticRoots ?? []).map(root => root.dir)] }
|
|
106
104
|
}
|
|
107
|
-
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
108
107
|
// vite preview also loads with command 'serve'; pin it to the same
|
|
109
108
|
// port convention so E2E runs are deterministic.
|
|
110
|
-
preview: portPin,
|
|
109
|
+
...(portPin && { preview: portPin }),
|
|
111
110
|
resolve: {
|
|
112
111
|
alias: { '@': path.resolve(options.dirname, './src') },
|
|
113
112
|
dedupe: FRAMEWORK
|
|
@@ -131,13 +130,13 @@ export const vueSpa =
|
|
|
131
130
|
exclude: allDeps.filter(isSourcePackage),
|
|
132
131
|
include: [...new Set([...FRAMEWORK.filter(name => allDeps.includes(name)), ...nestedDependencies(options.dirname, deps, isSourcePackage)])]
|
|
133
132
|
},
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
...(options.scssLoadPaths && {
|
|
134
|
+
css: {
|
|
135
|
+
preprocessorOptions: {
|
|
136
|
+
scss: { loadPaths: options.scssLoadPaths }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}),
|
|
141
140
|
build: {
|
|
142
141
|
rolldownOptions: {
|
|
143
142
|
output: {
|
package/src/sourceDeps.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
4
|
// The parts of a package.json this walk reads.
|
|
@@ -16,11 +16,14 @@ export interface DeclaredDependencies {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// Where the installer put a package: the nearest node_modules at or above
|
|
19
|
-
// `from` holding it.
|
|
19
|
+
// `from` holding it, as the real path behind whatever link reached it. That is
|
|
20
|
+
// where its own dependencies are found from — an isolated install (bun's, pnpm's)
|
|
21
|
+
// puts them beside the package's real directory, not beside the link an app's
|
|
22
|
+
// node_modules holds — and it is what Node and Vite resolve from too.
|
|
20
23
|
const packageDir = (from: string, name: string): string => {
|
|
21
24
|
for (let dir = from; ; dir = path.dirname(dir)) {
|
|
22
25
|
const candidate = path.join(dir, 'node_modules', name)
|
|
23
|
-
if (existsSync(path.join(candidate, 'package.json'))) return candidate
|
|
26
|
+
if (existsSync(path.join(candidate, 'package.json'))) return realpathSync(candidate)
|
|
24
27
|
if (path.dirname(dir) === dir) throw new Error(`${name} is declared but not installed anywhere above ${from}`)
|
|
25
28
|
}
|
|
26
29
|
}
|
package/src/staticRoots.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const staticRoots = (roots: StaticRoot[]): Plugin => ({
|
|
|
42
42
|
|
|
43
43
|
configureServer(server) {
|
|
44
44
|
server.middlewares.use((req, res, next) => {
|
|
45
|
-
const url = (req.url ?? '').
|
|
45
|
+
const url = (req.url ?? '').replace(/\?.*/, '')
|
|
46
46
|
const root = roots.find(r => url.startsWith(`${r.urlPath}/`))
|
|
47
47
|
if (!root) {
|
|
48
48
|
next()
|