@greendrake/vite-preset 0.2.0 → 0.4.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 +5 -10
- package/package.json +3 -6
- package/src/html.ts +0 -21
- package/src/main.ts +8 -10
- package/src/sourceDeps.ts +3 -2
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;
|
|
@@ -50,7 +49,7 @@ What the preset sets:
|
|
|
50
49
|
vueSpa({ dirname: import.meta.dirname, sourcePackages: ['@greendrake/', '@acme/ui', 'my-design-system'] })
|
|
51
50
|
```
|
|
52
51
|
|
|
53
|
-
Excluding a package also hides it from Vite's dependency scanner, which externalises every excluded id and so never reads what that package imports. The third-party packages behind a source package are therefore pinned into the pre-bundle too, or the first browser request for one is a discovery: Vite re-runs the optimiser mid-session and full-reloads every open page — which under a parallel E2E run lands in the middle of tests in flight. The preset walks the app's runtime dependencies, follows the source packages among them, and pins each package they depend on, plus each peer they declare that the app itself supplies at runtime. Each is named in Vite's `parent > child` form unless the app declares it too, in which case its plain name — the id the scanner itself would produce — is used instead.
|
|
52
|
+
Excluding a package also hides it from Vite's dependency scanner, which externalises every excluded id and so never reads what that package imports. The third-party packages behind a source package are therefore pinned into the pre-bundle too, or the first browser request for one is a discovery: Vite re-runs the optimiser mid-session and full-reloads every open page — which under a parallel E2E run lands in the middle of tests in flight. The preset walks the app's runtime dependencies, follows the source packages among them, and pins each package they depend on, plus each peer they declare that the app itself supplies at runtime. Each is named in Vite's `parent > child` form unless the app declares it too, in which case its plain name — the id the scanner itself would produce — is used instead. Three things are left out: a package published only under subpaths, which no `optimizeDeps` entry can name and which Vite reaches through the subpath ids that import it anyway; a peer that is itself a source package, which the app's own declaration of it puts under the same rules as any other source package — excluded, and walked in its turn; and anything behind a source package the app keeps among its dev dependencies, which by the app's own reckoning is tooling (a build-tool integration shipped beside a package's components, a native-build helper) that browser code never reaches. That split is what an app is asserting when it puts a package in one list rather than the other, so put build-time source packages in `devDependencies`.
|
|
54
53
|
|
|
55
54
|
- `build`: font files (`woff2`, `woff`, `eot`, `ttf`, `otf`) are emitted unhashed under `assets/fonts/`, so `index.html` can prefetch them by a stable name; everything else is hashed.
|
|
56
55
|
|
|
@@ -75,7 +74,7 @@ An HMR update whose accepted module sits inside an import cycle cannot be re-exe
|
|
|
75
74
|
import { loadAppEnv, requirePort, envBool, deriveImageBaseUrl, apiTargetEnv, buildHash } from '@greendrake/vite-preset'
|
|
76
75
|
```
|
|
77
76
|
|
|
78
|
-
- `loadAppEnv(mode, dirname)` — the app's `.env` / `.env.<mode>` files, every key (not only `VITE_`-prefixed ones): they carry plain build inputs, and the `export` prefix they may be written with is stripped so the same file can be shell-sourced. `process.env` outranks the file. Do not name the files `.env.development` / `.env.production`: bun auto-loads those into `process.env` at startup, and whichever one it picked would then outrank the mode's file.
|
|
77
|
+
- `loadAppEnv(mode, dirname)` — the app's `.env` / `.env.<mode>` files, every key (not only `VITE_`-prefixed ones): they carry plain build inputs, and the `export` prefix they may be written with is stripped so the same file can be shell-sourced. `process.env` outranks the file. Do not name the files `.env.development` / `.env.production`: bun auto-loads those into `process.env` at startup, and whichever one it picked would then outrank the mode's file.
|
|
79
78
|
- `requirePort()` — `VITE_PORT`, required; the single port convention.
|
|
80
79
|
- `envBool(value)` — `"1"`, `"t"`, `"true"` (any case) are true; `"0"`, `"false"`, unset and everything else are false. `Boolean(process.env.X)` would take the literal string `"0"` as true.
|
|
81
80
|
- `deriveImageBaseUrl(env = process.env)` — the prefix for object-storage image URLs. A defined `S3_PREFIX` wins outright, even when empty; otherwise `AWS_S3_ENDPOINT/AWS_S3_BUCKET`; otherwise `''`.
|
|
@@ -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.
|
|
@@ -112,12 +107,12 @@ The single source of truth for where the API lives, pure and environment-free, s
|
|
|
112
107
|
|
|
113
108
|
## Sass: import shared style packages by bare specifier
|
|
114
109
|
|
|
115
|
-
`@use '@
|
|
110
|
+
`@use '@acme/font'` — never `@use 'pkg:@acme/font'`, and do not register Sass's `NodePackageImporter`.
|
|
116
111
|
|
|
117
112
|
Vite resolves bare specifiers with its own Sass importer and rebases relative `url()` references inside the imported package against that package's own source location, so `@font-face { src: url('./fonts/X.woff2') }` in a font package resolves, gets emitted into `dist/assets/`, and the emitted CSS points at the built asset.
|
|
118
113
|
|
|
119
114
|
`NodePackageImporter` resolves the same `@use` to a path Vite's CSS pipeline does not rebase against. The `url()` then survives verbatim into the output, no font asset is emitted, and the build warns `didn't resolve at build time, it will remain unchanged to be resolved at runtime` — every font 404s at runtime. This is a property of the importer, not of any Vite version.
|
|
120
115
|
|
|
121
|
-
The corollary
|
|
116
|
+
The corollary: the `url()` literal must be written in the owning package's own `.scss`, because that is the file the rebasing is relative to — a shared `@font-face` mixin that builds the URL itself breaks it.
|
|
122
117
|
|
|
123
118
|
If a future Vite drops bare-specifier support, migrate to `loadPaths` or move the `@font-face` blocks into the consuming apps — not to `pkg:`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@greendrake/vite-preset",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.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/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 —
|
|
@@ -19,23 +18,3 @@ export const htmlPlaceholders = (map: Record<string, string>): Plugin => {
|
|
|
19
18
|
}
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
// Production-only HTML minification — strips comments and collapses
|
|
24
|
-
// whitespace in index.html, and runs inline scripts/styles through
|
|
25
|
-
// terser/CSS minification. Dev keeps the source readable.
|
|
26
|
-
export const htmlMinify = (): Plugin => ({
|
|
27
|
-
name: 'greendrake-html-minify',
|
|
28
|
-
apply: 'build',
|
|
29
|
-
transformIndexHtml: {
|
|
30
|
-
order: 'post',
|
|
31
|
-
handler: html =>
|
|
32
|
-
minify(html, {
|
|
33
|
-
collapseWhitespace: true,
|
|
34
|
-
removeComments: true,
|
|
35
|
-
minifyJS: true,
|
|
36
|
-
minifyCSS: true,
|
|
37
|
-
useShortDoctype: true,
|
|
38
|
-
removeRedundantAttributes: true
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
})
|
package/src/main.ts
CHANGED
|
@@ -4,15 +4,15 @@ import vue from '@vitejs/plugin-vue'
|
|
|
4
4
|
import { mergeConfig, type ConfigEnv, type UserConfig, type UserConfigFnObject } from 'vite'
|
|
5
5
|
|
|
6
6
|
export { envBool, deriveImageBaseUrl, loadAppEnv, requirePort, apiTargetEnv, buildHash } from './env'
|
|
7
|
-
// Re-exported so config factories building on vueSpa
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Re-exported so config factories building on vueSpa speak this package's
|
|
8
|
+
// vite types: a second vite copy resolved downstream is a structurally
|
|
9
|
+
// 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'
|
|
@@ -28,14 +28,12 @@ export interface VueSpaOptions {
|
|
|
28
28
|
scssLoadPaths?: string[]
|
|
29
29
|
// 'env' (default): the strict VITE_PORT convention, required when the dev
|
|
30
30
|
// server starts. A number: that port verbatim, for callers that resolved
|
|
31
|
-
// the app's env themselves
|
|
32
|
-
//
|
|
31
|
+
// the app's env themselves. false: no port pinning — for simple apps
|
|
32
|
+
// served on any free port.
|
|
33
33
|
port?: 'env' | number | false
|
|
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
|
|
@@ -90,7 +88,7 @@ export const vueSpa =
|
|
|
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
90
|
...(options.define && { define: Object.fromEntries(Object.entries(options.define).map(([key, value]) => [key, JSON.stringify(value)])) }),
|
|
93
|
-
plugins: [vue(), hmrCircularImportGuard(), ...(options.staticRoots?.length ? [staticRootsPlugin(options.staticRoots)] : []), ...(options.htmlPlaceholders ? [htmlPlaceholders(options.htmlPlaceholders)] : [])
|
|
91
|
+
plugins: [vue(), hmrCircularImportGuard(), ...(options.staticRoots?.length ? [staticRootsPlugin(options.staticRoots)] : []), ...(options.htmlPlaceholders ? [htmlPlaceholders(options.htmlPlaceholders)] : [])],
|
|
94
92
|
...(command === 'serve'
|
|
95
93
|
? {
|
|
96
94
|
server: {
|
package/src/sourceDeps.ts
CHANGED
|
@@ -86,9 +86,10 @@ export const nestedDependencies = (dirname: string, declared: DeclaredDependenci
|
|
|
86
86
|
}
|
|
87
87
|
// A peer is the app's to supply, so it counts only where the app does,
|
|
88
88
|
// at runtime — the same reckoning, applied to what a package leaves to
|
|
89
|
-
// its consumer.
|
|
89
|
+
// its consumer. A source package among them is served raw like any
|
|
90
|
+
// other, and walked as the app's own declaration of it is.
|
|
90
91
|
for (const peer of Object.keys(pkg.peerDependencies ?? {})) {
|
|
91
|
-
if (declared.runtime.includes(peer)) pin(here, dirname, peer)
|
|
92
|
+
if (declared.runtime.includes(peer) && !isSourcePackage(peer)) pin(here, dirname, peer)
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
95
|
for (const name of declared.runtime) {
|