@greendrake/vite-preset 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greendrake/vite-preset",
3
- "version": "0.1.0",
3
+ "version": "0.2.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,7 +38,7 @@
38
38
  },
39
39
  "prettier": "@greendrake/dev-config/prettier",
40
40
  "dependencies": {
41
- "@greendrake/util": "0.1.0",
41
+ "@greendrake/util": "0.2.0",
42
42
  "@vitejs/plugin-vue": "^6.0.7",
43
43
  "html-minifier-terser": "^7.2.0"
44
44
  },
@@ -46,7 +46,7 @@
46
46
  "vite": ">=8"
47
47
  },
48
48
  "devDependencies": {
49
- "@greendrake/dev-config": "0.1.0",
49
+ "@greendrake/dev-config": "0.2.0",
50
50
  "@types/bun": "^1.3.5",
51
51
  "@types/html-minifier-terser": "^7.0.2",
52
52
  "@types/node": "^24.0.0",
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
@@ -14,7 +14,8 @@ export const htmlPlaceholders = (map: Record<string, string>): Plugin => {
14
14
  name: 'greendrake-html-placeholders',
15
15
  transformIndexHtml: {
16
16
  order: 'pre',
17
- handler: html => html.replace(pattern, match => map[match])
17
+ // The pattern is built from the map's own keys, so every match is one.
18
+ handler: html => html.replace(pattern, match => map[match]!)
18
19
  }
19
20
  }
20
21
  }
package/src/main.ts CHANGED
@@ -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
@@ -48,10 +48,10 @@ export interface VueSpaOptions {
48
48
  // source-shipped by construction — an app with source packages of its own
49
49
  // names them here, and must name the @greendrake scope again if it uses
50
50
  // any.
51
- sourcePackages?: string[]
51
+ sourcePackages?: string[] | undefined
52
52
  // Deep-merged last over the preset — the escape hatch for anything
53
53
  // app-specific (server.proxy, extra plugins, build tweaks).
54
- overrides?: UserConfig
54
+ overrides?: UserConfig | undefined
55
55
  }
56
56
 
57
57
  // The libraries the app and every component package it renders must share one
@@ -89,11 +89,11 @@ export const vueSpa =
89
89
  // none (and never serves) must not be made to produce one.
90
90
  const portPin = command === 'serve' && options.port !== false ? { port: typeof options.port === 'number' ? options.port : requirePort(), strictPort: true } : undefined
91
91
  const base: UserConfig = {
92
- define: options.define && Object.fromEntries(Object.entries(options.define).map(([key, value]) => [key, JSON.stringify(value)])),
92
+ ...(options.define && { define: Object.fromEntries(Object.entries(options.define).map(([key, value]) => [key, JSON.stringify(value)])) }),
93
93
  plugins: [vue(), hmrCircularImportGuard(), ...(options.staticRoots?.length ? [staticRootsPlugin(options.staticRoots)] : []), ...(options.htmlPlaceholders ? [htmlPlaceholders(options.htmlPlaceholders)] : []), ...(options.htmlMinify ? [htmlMinify()] : [])],
94
- server:
95
- command === 'serve'
96
- ? {
94
+ ...(command === 'serve'
95
+ ? {
96
+ server: {
97
97
  ...portPin,
98
98
  // '..' reaches the workspace an app sits directly
99
99
  // inside. Anything the dev server must read from
@@ -104,10 +104,11 @@ export const vueSpa =
104
104
  // refused.
105
105
  fs: { allow: ['..', ...(options.staticRoots ?? []).map(root => root.dir)] }
106
106
  }
107
- : undefined,
107
+ }
108
+ : {}),
108
109
  // vite preview also loads with command 'serve'; pin it to the same
109
110
  // port convention so E2E runs are deterministic.
110
- preview: portPin,
111
+ ...(portPin && { preview: portPin }),
111
112
  resolve: {
112
113
  alias: { '@': path.resolve(options.dirname, './src') },
113
114
  dedupe: FRAMEWORK
@@ -131,13 +132,13 @@ export const vueSpa =
131
132
  exclude: allDeps.filter(isSourcePackage),
132
133
  include: [...new Set([...FRAMEWORK.filter(name => allDeps.includes(name)), ...nestedDependencies(options.dirname, deps, isSourcePackage)])]
133
134
  },
134
- css: options.scssLoadPaths
135
- ? {
136
- preprocessorOptions: {
137
- scss: { loadPaths: options.scssLoadPaths }
138
- }
139
- }
140
- : undefined,
135
+ ...(options.scssLoadPaths && {
136
+ css: {
137
+ preprocessorOptions: {
138
+ scss: { loadPaths: options.scssLoadPaths }
139
+ }
140
+ }
141
+ }),
141
142
  build: {
142
143
  rolldownOptions: {
143
144
  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()