@igstack/app-catalog-frontend-build-vite 0.0.1

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.
@@ -0,0 +1,178 @@
1
+ import viteReact from '@vitejs/plugin-react'
2
+ import * as fs from 'node:fs'
3
+ import * as path from 'node:path'
4
+ import * as process from 'node:process'
5
+ import type { UserConfig } from 'vite'
6
+ import type { ManifestOptions } from 'vite-plugin-pwa'
7
+ import { VitePWA } from 'vite-plugin-pwa'
8
+ import { viteStaticCopy } from 'vite-plugin-static-copy'
9
+ import svgr from 'vite-plugin-svgr'
10
+
11
+ export function frontendViteConfig(options?: {
12
+ appRoot?: string
13
+ pwa?: {
14
+ manifest?: Partial<ManifestOptions>
15
+ registerType?: 'autoUpdate' | 'prompt'
16
+ selfDestroying?: boolean
17
+ }
18
+ }) {
19
+ const plugins: UserConfig['plugins'] = []
20
+
21
+ // Set up static copy for public assets if appRoot is provided
22
+ if (options?.appRoot) {
23
+ const appPublicDir = path.join(options.appRoot, 'public')
24
+
25
+ // Copy core public assets first, then local public assets (local takes precedence)
26
+ plugins.push(
27
+ viteStaticCopy({
28
+ targets: [
29
+ {
30
+ src: 'node_modules/@igstack/app-catalog-frontend-core/public/[!.]*',
31
+ dest: '.',
32
+ },
33
+ ...(fs.existsSync(appPublicDir)
34
+ ? [
35
+ {
36
+ src: 'public/[!.]*',
37
+ dest: '.',
38
+ },
39
+ ]
40
+ : []),
41
+ ],
42
+ }),
43
+ )
44
+ }
45
+
46
+ // Configure VitePWA
47
+ const registerType =
48
+ options?.pwa?.registerType ||
49
+ (process.env['VITE_AUTO_UPDATE'] === 'true' ? 'autoUpdate' : 'prompt')
50
+
51
+ const selfDestroying =
52
+ options?.pwa?.selfDestroying !== undefined
53
+ ? options.pwa.selfDestroying
54
+ : process.env['VITE_SELF_DESTROYING'] === 'true'
55
+ ? true
56
+ : undefined
57
+
58
+ plugins.push(
59
+ VitePWA({
60
+ registerType,
61
+ selfDestroying,
62
+ workbox: {
63
+ globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
64
+ },
65
+ includeAssets: [
66
+ 'favicon.ico',
67
+ 'apple-touch-180x180.png',
68
+ 'app-catalog-*.png',
69
+ 'app-catalog-square.svg',
70
+ ],
71
+ manifest: {
72
+ name: 'App Catalog',
73
+ short_name: 'EH',
74
+ description: 'Jump between environments',
75
+ theme_color: '#1f2937',
76
+ background_color: '#ffffff',
77
+ display: 'standalone',
78
+ start_url: '/',
79
+ icons: [
80
+ {
81
+ src: 'favicon.ico',
82
+ sizes: '48x48',
83
+ type: 'image/x-icon',
84
+ },
85
+ {
86
+ src: 'app-catalog-16x16.png',
87
+ sizes: '16x16',
88
+ type: 'image/png',
89
+ },
90
+ {
91
+ src: 'app-catalog-32x32.png',
92
+ sizes: '32x32',
93
+ type: 'image/png',
94
+ },
95
+ {
96
+ src: 'app-catalog-48x48.png',
97
+ sizes: '48x48',
98
+ type: 'image/png',
99
+ },
100
+ {
101
+ src: 'app-catalog-192x192.png',
102
+ sizes: '192x192',
103
+ type: 'image/png',
104
+ },
105
+ {
106
+ src: 'app-catalog-512x512.png',
107
+ sizes: '512x512',
108
+ type: 'image/png',
109
+ },
110
+ {
111
+ src: 'app-catalog-square.svg',
112
+ sizes: '150x150',
113
+ type: 'image/svg+xml',
114
+ },
115
+ {
116
+ src: 'app-catalog-square.svg',
117
+ sizes: '150x150',
118
+ purpose: 'maskable',
119
+ type: 'image/svg+xml',
120
+ },
121
+ ],
122
+ ...options?.pwa?.manifest,
123
+ },
124
+ devOptions: {
125
+ enabled: true,
126
+ },
127
+ }),
128
+ )
129
+
130
+ plugins.push(svgr())
131
+ plugins.push(viteReact())
132
+
133
+ if (process.env['NODE_ENV'] === 'test') {
134
+ plugins.push({
135
+ name: 'load-svg',
136
+ enforce: 'pre',
137
+ transform(_: string, id: string) {
138
+ if (id.endsWith('.svg?react')) {
139
+ return `export default () => "svg-stub"`
140
+ }
141
+ return undefined
142
+ },
143
+ })
144
+ }
145
+
146
+ // plugins.push(tanstackRouter());
147
+
148
+ return {
149
+ server: {
150
+ port: 4000,
151
+ strictPort: true,
152
+ host: 'localhost',
153
+ proxy: {
154
+ '/api': {
155
+ target: 'http://localhost:4001',
156
+ changeOrigin: true,
157
+ cookieDomainRewrite: 'localhost',
158
+ },
159
+ '/trpc': {
160
+ target: 'http://localhost:4001',
161
+ changeOrigin: true,
162
+ cookieDomainRewrite: 'localhost',
163
+ },
164
+ '/static': {
165
+ target: 'http://localhost:4001',
166
+ changeOrigin: true,
167
+ },
168
+ },
169
+ },
170
+ resolve: {
171
+ // Use 'my-custom-condition' to resolve @igstack/app-catalog-frontend-core to source files
172
+ // This enables HMR when developing the core library alongside the consuming app
173
+ conditions: ['my-custom-condition'],
174
+ },
175
+ publicDir: '.vite-merged-public',
176
+ plugins,
177
+ } satisfies UserConfig
178
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { frontendViteConfig } from './frontendViteConfig'
2
+ export {
3
+ watchExternalSource,
4
+ createSourceAliases,
5
+ createFsAllowPaths,
6
+ type WatchExternalSourceOptions,
7
+ } from './watchExternalSource'
@@ -0,0 +1,177 @@
1
+ import type { Plugin, ViteDevServer } from 'vite'
2
+
3
+ export interface WatchExternalSourceOptions {
4
+ /**
5
+ * Name to display in console logs (e.g., '@igstack/app-catalog-frontend-core')
6
+ */
7
+ name: string
8
+ /**
9
+ * Absolute path to the source directory to watch
10
+ */
11
+ srcPath: string
12
+ /**
13
+ * Polling interval in ms (default: 500)
14
+ * Polling is required for cross-monorepo watching
15
+ */
16
+ interval?: number
17
+ }
18
+
19
+ /**
20
+ * Creates a Vite plugin that watches an external source directory for changes
21
+ * and triggers HMR updates or full reloads.
22
+ *
23
+ * This is useful when developing with linked packages from another monorepo
24
+ * where Vite's default file watcher doesn't detect changes.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * // In vite.config.ts
29
+ * import { watchExternalSource } from '@igstack/app-catalog-frontend-build-vite'
30
+ * import path from 'node:path'
31
+ *
32
+ * export default defineConfig({
33
+ * plugins: [
34
+ * watchExternalSource({
35
+ * name: '@igstack/app-catalog-frontend-core',
36
+ * srcPath: path.resolve(__dirname, '../../../app-catalog/packages/frontend-core/src'),
37
+ * }),
38
+ * ],
39
+ * })
40
+ * ```
41
+ */
42
+ export function watchExternalSource(
43
+ options: WatchExternalSourceOptions,
44
+ ): Plugin {
45
+ const { name, srcPath, interval = 500 } = options
46
+ let watcher: import('chokidar').FSWatcher | null = null // eslint-disable-line @typescript-eslint/consistent-type-imports
47
+
48
+ return {
49
+ name: `watch-external-source:${name}`,
50
+ configureServer(server: ViteDevServer) {
51
+
52
+ import('chokidar').then(({ watch }) => {
53
+ watcher = watch(srcPath, {
54
+ ignoreInitial: true,
55
+ usePolling: true, // Required for cross-monorepo watching
56
+ interval,
57
+ })
58
+
59
+ watcher.on('ready', () => {
60
+ console.log(`[HMR] Watching ${name} for changes`)
61
+ })
62
+
63
+ watcher.on('change', async (file: string) => {
64
+ const shortPath = file.replace(srcPath, `${name}/src`)
65
+ console.log(`[HMR] ${shortPath} changed`)
66
+
67
+ // Try multiple ways to find the module in Vite's graph
68
+ let modules = server.moduleGraph.getModulesByFile(file)
69
+
70
+ // If not found, try searching by URL patterns
71
+ if (!modules || modules.size === 0) {
72
+ const allModules = [...server.moduleGraph.idToModuleMap.values()]
73
+ const fileName = file.split('/').pop() || ''
74
+ const matchingModules = allModules.filter((mod) => {
75
+ if (!mod.file) return false
76
+ return (
77
+ mod.file === file ||
78
+ mod.file.endsWith(fileName) ||
79
+ mod.url.includes(fileName)
80
+ )
81
+ })
82
+ if (matchingModules.length > 0) {
83
+ modules = new Set(matchingModules)
84
+ }
85
+ }
86
+
87
+ if (modules && modules.size > 0) {
88
+ // Module is tracked - try true HMR
89
+ const updates: Array<{
90
+ type: 'js-update' | 'css-update'
91
+ path: string
92
+ acceptedPath: string
93
+ timestamp: number
94
+ }> = []
95
+
96
+ for (const mod of modules) {
97
+ server.moduleGraph.invalidateModule(mod)
98
+ const isCss = mod.file?.endsWith('.css')
99
+
100
+ updates.push({
101
+ type: isCss ? 'css-update' : 'js-update',
102
+ path: mod.url,
103
+ acceptedPath: mod.url,
104
+ timestamp: Date.now(),
105
+ })
106
+
107
+ console.log(`[HMR] Sending update for: ${mod.url}`)
108
+ }
109
+
110
+ if (updates.length > 0) {
111
+ server.ws.send({
112
+ type: 'update',
113
+ updates,
114
+ })
115
+ console.log(`[HMR] Sent ${updates.length} HMR update(s)`)
116
+ return
117
+ }
118
+ }
119
+
120
+ // Module not in graph - fall back to full reload
121
+ console.log(`[HMR] Module not tracked, triggering full reload`)
122
+ server.moduleGraph.invalidateAll()
123
+ server.ws.send({
124
+ type: 'full-reload',
125
+ path: '*',
126
+ })
127
+ })
128
+
129
+ watcher.on('error', (error: unknown) => {
130
+ console.error(`[HMR] Watcher error for ${name}:`, error)
131
+ })
132
+ })
133
+ },
134
+ closeBundle() {
135
+ if (watcher) {
136
+ watcher.close()
137
+ }
138
+ },
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Creates resolve aliases for pointing to source files instead of dist.
144
+ * Use this with watchExternalSource for full HMR support.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * // In vite.config.ts
149
+ * import { createSourceAliases } from '@igstack/app-catalog-frontend-build-vite'
150
+ * import path from 'node:path'
151
+ *
152
+ * const frontendCoreSrc = path.resolve(__dirname, '../../../app-catalog/packages/frontend-core/src')
153
+ *
154
+ * export default defineConfig({
155
+ * resolve: {
156
+ * alias: createSourceAliases({
157
+ * '@igstack/app-catalog-frontend-core': `${frontendCoreSrc}/index.tsx`,
158
+ * '~': frontendCoreSrc, // Internal path alias used by frontend-core
159
+ * }),
160
+ * },
161
+ * })
162
+ * ```
163
+ */
164
+ export function createSourceAliases(
165
+ aliases: Record<string, string>,
166
+ ): Record<string, string> {
167
+ return aliases
168
+ }
169
+
170
+ /**
171
+ * Creates fs.allow paths for serving files from external directories.
172
+ *
173
+ * @param paths - Array of absolute paths to allow
174
+ */
175
+ export function createFsAllowPaths(paths: Array<string>): Array<string> {
176
+ return paths
177
+ }