@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 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`, `htmlPlaceholders` and `htmlMinify` plugins when their options are given;
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.1.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
- "@greendrake/util": "0.1.0",
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.1.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
- apiPort?: string
21
- publicFeHost?: string
22
- publicApiHost?: string
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
- handler: html => html.replace(pattern, match => map[match])
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, htmlMinify } from './html'
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, htmlMinify } from './html'
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
- define: options.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)] : []), ...(options.htmlMinify ? [htmlMinify()] : [])],
94
- server:
95
- command === 'serve'
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
- : undefined,
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
- css: options.scssLoadPaths
135
- ? {
136
- preprocessorOptions: {
137
- scss: { loadPaths: options.scssLoadPaths }
138
- }
139
- }
140
- : undefined,
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
  }
@@ -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 ?? '').split('?')[0]
45
+ const url = (req.url ?? '').replace(/\?.*/, '')
46
46
  const root = roots.find(r => url.startsWith(`${r.urlPath}/`))
47
47
  if (!root) {
48
48
  next()