@tothalex/nulljs 0.0.48 → 0.0.53

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.
Files changed (58) hide show
  1. package/package.json +22 -32
  2. package/src/cli.ts +24 -0
  3. package/src/commands/config.ts +130 -0
  4. package/src/commands/deploy.ts +182 -123
  5. package/src/commands/dev.ts +10 -0
  6. package/src/commands/host.ts +130 -139
  7. package/src/commands/index.ts +6 -8
  8. package/src/commands/secret.ts +364 -56
  9. package/src/commands/status.ts +41 -0
  10. package/src/components/DeployAnimation.tsx +92 -0
  11. package/src/components/DeploymentLogsPane.tsx +79 -0
  12. package/src/components/Header.tsx +57 -0
  13. package/src/components/HelpModal.tsx +64 -0
  14. package/src/components/SystemLogsPane.tsx +78 -0
  15. package/src/config/index.ts +181 -0
  16. package/src/lib/bundle/function.ts +125 -0
  17. package/src/lib/bundle/index.ts +3 -0
  18. package/src/lib/bundle/react.ts +149 -0
  19. package/src/lib/deploy.ts +103 -0
  20. package/src/lib/server.ts +160 -0
  21. package/src/lib/vite.ts +120 -0
  22. package/src/lib/watcher.ts +274 -0
  23. package/src/ui.tsx +363 -0
  24. package/tsconfig.json +30 -0
  25. package/scripts/install-server.js +0 -199
  26. package/src/commands/api.ts +0 -16
  27. package/src/commands/auth.ts +0 -54
  28. package/src/commands/create.ts +0 -43
  29. package/src/commands/dev/function/index.ts +0 -221
  30. package/src/commands/dev/function/utils.ts +0 -99
  31. package/src/commands/dev/index.tsx +0 -126
  32. package/src/commands/dev/logging-manager.ts +0 -87
  33. package/src/commands/dev/server/index.ts +0 -48
  34. package/src/commands/dev/server/utils.ts +0 -37
  35. package/src/commands/dev/ui/components/scroll-area.tsx +0 -141
  36. package/src/commands/dev/ui/components/tab-bar.tsx +0 -67
  37. package/src/commands/dev/ui/index.tsx +0 -71
  38. package/src/commands/dev/ui/logging-context.tsx +0 -76
  39. package/src/commands/dev/ui/tabs/functions-tab.tsx +0 -35
  40. package/src/commands/dev/ui/tabs/server-tab.tsx +0 -36
  41. package/src/commands/dev/ui/tabs/vite-tab.tsx +0 -35
  42. package/src/commands/dev/ui/use-logging.tsx +0 -34
  43. package/src/commands/dev/vite/index.ts +0 -54
  44. package/src/commands/dev/vite/utils.ts +0 -71
  45. package/src/commands/profile.ts +0 -189
  46. package/src/index.ts +0 -346
  47. package/src/lib/api.ts +0 -189
  48. package/src/lib/bundle/function/index.ts +0 -46
  49. package/src/lib/bundle/react/index.ts +0 -2
  50. package/src/lib/bundle/react/spa.ts +0 -77
  51. package/src/lib/bundle/react/ssr/client.ts +0 -93
  52. package/src/lib/bundle/react/ssr/config.ts +0 -77
  53. package/src/lib/bundle/react/ssr/index.ts +0 -4
  54. package/src/lib/bundle/react/ssr/props.ts +0 -71
  55. package/src/lib/bundle/react/ssr/server.ts +0 -83
  56. package/src/lib/config.ts +0 -347
  57. package/src/lib/deployment.ts +0 -244
  58. package/src/lib/update-server.ts +0 -262
@@ -0,0 +1,57 @@
1
+ import type { BinarySource } from '../lib/server'
2
+
3
+ type Tab = 'system' | 'deployment'
4
+
5
+ type HeaderProps = {
6
+ activeTab: Tab
7
+ functionCount: number
8
+ viteRunning: boolean
9
+ isDeploying: boolean
10
+ binarySource: BinarySource
11
+ }
12
+
13
+ const getBinarySourceLabel = (source: BinarySource): string => {
14
+ switch (source) {
15
+ case 'local-debug':
16
+ return 'local (debug)'
17
+ case 'local-release':
18
+ return 'local (release)'
19
+ case 'local-env':
20
+ return 'local (env)'
21
+ case 'npm':
22
+ return 'npm'
23
+ }
24
+ }
25
+
26
+ export const Header = (props: HeaderProps) => {
27
+ const { activeTab, functionCount, viteRunning, isDeploying, binarySource } = props
28
+
29
+ return (
30
+ <box flexDirection="row" justifyContent="space-between">
31
+ <box flexDirection="row">
32
+ <text fg={activeTab === 'system' ? 'cyan' : undefined} content="[1] System Logs" />
33
+ <text content=" | " />
34
+ <text fg={activeTab === 'deployment' ? 'cyan' : undefined} content="[2] Deployment Logs" />
35
+ </box>
36
+ <box flexDirection="row">
37
+ {isDeploying ? (
38
+ <text fg="yellow" content="● deploying all..." />
39
+ ) : (
40
+ <text fg={functionCount > 0 ? 'green' : 'yellow'} content="● watching" />
41
+ )}
42
+ <text fg="gray" content={` (${functionCount} functions)`} />
43
+ {viteRunning && (
44
+ <>
45
+ <text fg="gray" content=" | vite: " />
46
+ <text fg="magenta" content=":5173" />
47
+ </>
48
+ )}
49
+ <text fg="gray" content=" | server: " />
50
+ <text
51
+ fg={binarySource.startsWith('local') ? 'yellow' : 'green'}
52
+ content={getBinarySourceLabel(binarySource)}
53
+ />
54
+ </box>
55
+ </box>
56
+ )
57
+ }
@@ -0,0 +1,64 @@
1
+ type HelpModalProps = {
2
+ visible: boolean
3
+ }
4
+
5
+ const KeyBinding = ({ keys, description }: { keys: string; description: string }) => (
6
+ <text>
7
+ {' '}
8
+ <span fg="green">{keys}</span>
9
+ <span fg="gray"> - </span>
10
+ {description}
11
+ </text>
12
+ )
13
+
14
+ const Section = ({ title }: { title: string }) => (
15
+ <text>
16
+ <span fg="gray">{title}</span>
17
+ </text>
18
+ )
19
+
20
+ export const HelpModal = ({ visible }: HelpModalProps) => {
21
+ if (!visible) return null
22
+
23
+ return (
24
+ <box
25
+ position="absolute"
26
+ top={0}
27
+ left={0}
28
+ width="100%"
29
+ height="100%"
30
+ backgroundColor="black"
31
+ opacity={0.9}
32
+ justifyContent="center"
33
+ alignItems="center">
34
+ <box
35
+ width={50}
36
+ height={18}
37
+ border
38
+ borderStyle="rounded"
39
+ flexDirection="column"
40
+ padding={1}
41
+ backgroundColor="black">
42
+ <text>
43
+ <span fg="cyan">
44
+ <strong>Keyboard Shortcuts</strong>
45
+ </span>
46
+ </text>
47
+ <text />
48
+ <Section title="Navigation" />
49
+ <KeyBinding keys="1/2" description="Switch tabs" />
50
+ <KeyBinding keys="h/l" description="Previous/next tab" />
51
+ <KeyBinding keys="Tab" description="Cycle tabs" />
52
+ <text />
53
+ <Section title="Scrolling" />
54
+ <KeyBinding keys="j/k" description="Scroll down/up" />
55
+ <KeyBinding keys="g/G" description="Top/bottom" />
56
+ <text />
57
+ <Section title="Actions" />
58
+ <KeyBinding keys="d" description="Deploy all" />
59
+ <KeyBinding keys="?" description="Toggle help" />
60
+ <KeyBinding keys="Esc" description="Close help" />
61
+ </box>
62
+ </box>
63
+ )
64
+ }
@@ -0,0 +1,78 @@
1
+ import type { SystemLogEntry } from '@nulljs/api'
2
+
3
+ interface SystemLogsPaneProps {
4
+ logs: SystemLogEntry[]
5
+ loading: boolean
6
+ autoScroll: boolean
7
+ jumpTrigger: number
8
+ }
9
+
10
+ const getLevelColor = (level: string): string => {
11
+ switch (level.toLowerCase()) {
12
+ case 'error':
13
+ return 'red'
14
+ case 'warn':
15
+ case 'warning':
16
+ return 'yellow'
17
+ case 'info':
18
+ return 'green'
19
+ case 'debug':
20
+ case 'trace':
21
+ return 'gray'
22
+ default:
23
+ return 'white'
24
+ }
25
+ }
26
+
27
+ const sanitizeMessage = (message: string): string => {
28
+ return message
29
+ .replace(/\x1b\[[0-9;]*m/g, '')
30
+ .replace(/[\r\n]+/g, ' ')
31
+ .replace(/\s+/g, ' ')
32
+ .trim()
33
+ }
34
+
35
+ export const SystemLogsPane = (props: SystemLogsPaneProps) => {
36
+ if (props.loading && props.logs.length === 0) {
37
+ return (
38
+ <box flexDirection="column" padding={1}>
39
+ <text>Loading system logs...</text>
40
+ </box>
41
+ )
42
+ }
43
+
44
+ if (!props.loading && props.logs.length === 0) {
45
+ return (
46
+ <box flexDirection="column" padding={1}>
47
+ <text>No system logs found</text>
48
+ </box>
49
+ )
50
+ }
51
+
52
+ // Reverse logs so oldest are at top, newest at bottom
53
+ const logsInOrder = [...props.logs].reverse()
54
+
55
+ return (
56
+ <scrollbox
57
+ focused
58
+ stickyStart={props.autoScroll ? 'bottom' : 'top'}
59
+ stickyScroll={props.autoScroll}
60
+ key={`${props.autoScroll}-${props.jumpTrigger}`}>
61
+ {logsInOrder.map((log) => {
62
+ const time = new Date(log.timestamp).toLocaleTimeString()
63
+ const timestamp = `[${time}]`
64
+ const level = log.level.toUpperCase()
65
+
66
+ return (
67
+ <box key={log.id} flexDirection="row">
68
+ <text>
69
+ <span fg="gray">{timestamp}</span>
70
+ <span fg={getLevelColor(log.level)}>{` ${level} `}</span>
71
+ <span>{sanitizeMessage(log.message)}</span>
72
+ </text>
73
+ </box>
74
+ )
75
+ })}
76
+ </scrollbox>
77
+ )
78
+ }
@@ -0,0 +1,181 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
2
+ import { join } from 'path'
3
+ import chalk from 'chalk'
4
+
5
+ export type Config = {
6
+ name: string
7
+ key: {
8
+ public: string
9
+ private: string
10
+ }
11
+ api: string
12
+ }
13
+
14
+ type ConfigStore = {
15
+ current: string
16
+ configs: Record<string, Config>
17
+ }
18
+
19
+ // Local config (project-level)
20
+ const getLocalConfigDir = () => join(process.cwd(), '.nulljs')
21
+ const getLocalConfigFile = () => join(getLocalConfigDir(), 'config.json')
22
+
23
+ const ensureLocalConfigDir = () => {
24
+ const localDir = getLocalConfigDir()
25
+ if (!existsSync(localDir)) {
26
+ mkdirSync(localDir, { recursive: true })
27
+ }
28
+ }
29
+
30
+ const readConfigStore = (): ConfigStore | null => {
31
+ try {
32
+ const localFile = getLocalConfigFile()
33
+ if (!existsSync(localFile)) {
34
+ return null
35
+ }
36
+ const data = readFileSync(localFile, 'utf-8')
37
+ return JSON.parse(data)
38
+ } catch {
39
+ return null
40
+ }
41
+ }
42
+
43
+ const writeConfigStore = (store: ConfigStore) => {
44
+ ensureLocalConfigDir()
45
+ writeFileSync(getLocalConfigFile(), JSON.stringify(store, null, 2))
46
+ }
47
+
48
+ export const readLocalConfig = (): Config | null => {
49
+ const store = readConfigStore()
50
+ if (!store) return null
51
+ return store.configs[store.current] ?? null
52
+ }
53
+
54
+ export const getConfig = (name: string): Config | null => {
55
+ const store = readConfigStore()
56
+ if (!store) return null
57
+ return store.configs[name] ?? null
58
+ }
59
+
60
+ export const listConfigs = (): { configs: Config[]; current: string } | null => {
61
+ const store = readConfigStore()
62
+ if (!store) return null
63
+ return {
64
+ configs: Object.values(store.configs),
65
+ current: store.current
66
+ }
67
+ }
68
+
69
+ export const useConfig = (name: string): boolean => {
70
+ const store = readConfigStore()
71
+ if (!store) {
72
+ console.error(chalk.red('✗ No configurations found. Run "nulljs dev" first.'))
73
+ return false
74
+ }
75
+ if (!store.configs[name]) {
76
+ console.error(chalk.red(`✗ Config "${name}" not found.`))
77
+ console.log(chalk.gray('Available configs:'))
78
+ Object.keys(store.configs).forEach((n) => {
79
+ console.log(chalk.gray(` - ${n}`))
80
+ })
81
+ return false
82
+ }
83
+ store.current = name
84
+ writeConfigStore(store)
85
+ console.log(chalk.green(`✓ Now using config "${name}"`))
86
+ return true
87
+ }
88
+
89
+ export const createConfig = async (name: string, api: string): Promise<Config> => {
90
+ const store = readConfigStore() ?? { current: 'dev', configs: {} }
91
+
92
+ if (store.configs[name]) {
93
+ console.error(chalk.red(`✗ Config "${name}" already exists.`))
94
+ process.exit(1)
95
+ }
96
+
97
+ const keyPair = await crypto.subtle.generateKey(
98
+ {
99
+ name: 'Ed25519',
100
+ namedCurve: 'Ed25519'
101
+ },
102
+ true,
103
+ ['sign', 'verify']
104
+ )
105
+
106
+ const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
107
+ const publicKeyBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey)
108
+
109
+ const privateKey = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)))
110
+ const publicKey = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)))
111
+
112
+ const newConfig: Config = {
113
+ name,
114
+ key: {
115
+ public: publicKey,
116
+ private: privateKey
117
+ },
118
+ api
119
+ }
120
+
121
+ store.configs[name] = newConfig
122
+ writeConfigStore(store)
123
+
124
+ console.log(chalk.green(`✓ Config "${name}" created`))
125
+ console.log(chalk.blue(' API:') + ` ${api}`)
126
+ console.log(chalk.blue(' Public Key:') + ` ${publicKey.substring(0, 30)}...`)
127
+
128
+ return newConfig
129
+ }
130
+
131
+ export const getOrCreateLocalDevConfig = async (api: string = 'http://localhost:3000'): Promise<Config> => {
132
+ const store = readConfigStore()
133
+
134
+ if (store?.configs['dev']) {
135
+ return store.configs['dev']
136
+ }
137
+
138
+ // Generate new key pair for dev
139
+ const keyPair = await crypto.subtle.generateKey(
140
+ {
141
+ name: 'Ed25519',
142
+ namedCurve: 'Ed25519'
143
+ },
144
+ true,
145
+ ['sign', 'verify']
146
+ )
147
+
148
+ const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
149
+ const publicKeyBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey)
150
+
151
+ const privateKey = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)))
152
+ const publicKey = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)))
153
+
154
+ const devConfig: Config = {
155
+ name: 'dev',
156
+ key: {
157
+ public: publicKey,
158
+ private: privateKey
159
+ },
160
+ api
161
+ }
162
+
163
+ const newStore: ConfigStore = store ?? { current: 'dev', configs: {} }
164
+ newStore.configs['dev'] = devConfig
165
+ newStore.current = 'dev'
166
+ writeConfigStore(newStore)
167
+
168
+ return devConfig
169
+ }
170
+
171
+ export const loadPrivateKey = async (config: Config): Promise<CryptoKey> => {
172
+ const privateKeyBytes = Uint8Array.from(atob(config.key.private), (c) => c.charCodeAt(0))
173
+
174
+ return crypto.subtle.importKey(
175
+ 'pkcs8',
176
+ privateKeyBytes,
177
+ { name: 'Ed25519', namedCurve: 'Ed25519' },
178
+ false,
179
+ ['sign']
180
+ )
181
+ }
@@ -0,0 +1,125 @@
1
+ import type { InlineConfig, Plugin, UserConfig } from 'vite'
2
+ import { external } from './external'
3
+ import type { PluginOptions } from './types'
4
+
5
+ const jsFunction = ({ filePath }: PluginOptions): Plugin => {
6
+ return {
7
+ name: 'nulljs-function-plugin',
8
+ apply: 'build',
9
+ config: async (config: UserConfig, { command }) => {
10
+ if (command !== 'build') {
11
+ return config
12
+ }
13
+
14
+ return {
15
+ build: {
16
+ rollupOptions: {
17
+ input: {
18
+ handler: filePath
19
+ },
20
+ external,
21
+ output: {
22
+ // preserveModules: false,
23
+ entryFileNames: '[name].js'
24
+ },
25
+ preserveEntrySignatures: 'strict'
26
+ }
27
+ }
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ export const functionConfig = (filePath: string): InlineConfig => {
34
+ return {
35
+ logLevel: 'error',
36
+ plugins: [
37
+ jsFunction({
38
+ filePath
39
+ })
40
+ ],
41
+ build: {
42
+ outDir: '/tmp',
43
+ minify: false
44
+ }
45
+ }
46
+ }
47
+
48
+ export type FunctionEntry = {
49
+ name: string
50
+ path: string
51
+ type: 'api' | 'cron' | 'event'
52
+ }
53
+
54
+ export type BuildResult = {
55
+ entry: FunctionEntry
56
+ code: string
57
+ }
58
+
59
+ export type WatchCallbacks = {
60
+ onBuildComplete: (results: BuildResult[]) => Promise<void>
61
+ }
62
+
63
+ const jsFunctionWatch = (entries: FunctionEntry[], callbacks: WatchCallbacks): Plugin => {
64
+ // Map output filename back to entry
65
+ const outputToEntry = new Map<string, FunctionEntry>()
66
+
67
+ return {
68
+ name: 'nulljs-function-watch-plugin',
69
+ apply: 'build',
70
+ config: async (config: UserConfig, { command }) => {
71
+ if (command !== 'build') {
72
+ return config
73
+ }
74
+
75
+ // Build input object from entries
76
+ const input: Record<string, string> = {}
77
+ for (const entry of entries) {
78
+ input[entry.name] = entry.path
79
+ outputToEntry.set(`${entry.name}.js`, entry)
80
+ }
81
+
82
+ return {
83
+ build: {
84
+ rollupOptions: {
85
+ input,
86
+ external,
87
+ output: {
88
+ entryFileNames: '[name].js'
89
+ },
90
+ preserveEntrySignatures: 'strict'
91
+ }
92
+ }
93
+ }
94
+ },
95
+ async writeBundle(_, bundle) {
96
+ const results: BuildResult[] = []
97
+
98
+ for (const [fileName, chunk] of Object.entries(bundle)) {
99
+ if (chunk.type !== 'chunk' || !chunk.isEntry) continue
100
+
101
+ const entry = outputToEntry.get(fileName)
102
+ if (!entry) continue
103
+
104
+ results.push({ entry, code: chunk.code })
105
+ }
106
+
107
+ await callbacks.onBuildComplete(results)
108
+ }
109
+ }
110
+ }
111
+
112
+ export const functionWatchConfig = (
113
+ entries: FunctionEntry[],
114
+ callbacks: WatchCallbacks
115
+ ): InlineConfig => {
116
+ return {
117
+ logLevel: 'error',
118
+ plugins: [jsFunctionWatch(entries, callbacks)],
119
+ build: {
120
+ outDir: '/tmp/nulljs-dev',
121
+ minify: false,
122
+ watch: {}
123
+ }
124
+ }
125
+ }
@@ -1,2 +1,5 @@
1
1
  export * from './react'
2
2
  export * from './function'
3
+
4
+ export const isReact = (file: string) => file.endsWith('.tsx')
5
+ export const isTypescript = (file: string) => file.endsWith('.ts')
@@ -0,0 +1,149 @@
1
+ import { basename, extname } from 'path'
2
+ import type { InlineConfig, Plugin, UserConfig } from 'vite'
3
+ import react from '@vitejs/plugin-react'
4
+ import tailwindcss from '@tailwindcss/vite'
5
+
6
+ import type { PluginOptions } from './types'
7
+
8
+ const jsConfig = ({ filePath }: PluginOptions): Plugin => {
9
+ const entry = basename(filePath, extname(filePath))
10
+ const virtualPrefix = `virtual:config/${entry}.tsx`
11
+
12
+ return {
13
+ name: 'nulljs-config-plugin',
14
+ apply: 'build',
15
+ config: async (config: UserConfig, { command }) => {
16
+ if (command !== 'build') {
17
+ return config
18
+ }
19
+
20
+ return {
21
+ build: {
22
+ ssr: true,
23
+ rollupOptions: {
24
+ input: {
25
+ [entry]: virtualPrefix
26
+ },
27
+ external: ['react', 'react/jsx-runtime'],
28
+ output: {
29
+ entryFileNames: '[name].js'
30
+ },
31
+ preserveEntrySignatures: 'strict'
32
+ }
33
+ }
34
+ }
35
+ },
36
+
37
+ resolveId: (id: string) => {
38
+ if (id === virtualPrefix) {
39
+ return id
40
+ }
41
+ return null
42
+ },
43
+
44
+ load: (id: string) => {
45
+ if (id === virtualPrefix) {
46
+ const script = `
47
+ import { config } from "${filePath.replace(/\\/g, '\\\\')}";
48
+
49
+ export default config;
50
+ `
51
+
52
+ return script
53
+ }
54
+ return null
55
+ },
56
+
57
+ transform: (code, id) => {
58
+ if (id === filePath) {
59
+ const configMatch = code.match(/export const config = \{[^}]*\};/)
60
+ return configMatch ? { code: configMatch[0], map: null } : null
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ export const spaConfigConfig = (filePath: string): InlineConfig => {
67
+ return {
68
+ logLevel: 'error',
69
+ plugins: [
70
+ jsConfig({
71
+ filePath
72
+ })
73
+ ],
74
+ build: {
75
+ outDir: '/tmp'
76
+ }
77
+ }
78
+ }
79
+
80
+ const jsSpa = ({ filePath }: PluginOptions): Plugin => {
81
+ const entry = basename(filePath, extname(filePath))
82
+ const virtualPrefix = `virtual:spa/${entry}.tsx`
83
+
84
+ return {
85
+ name: 'nulljs-spa-client-plugin',
86
+ apply: 'build',
87
+ config: async (config: UserConfig, { command }) => {
88
+ if (command !== 'build') {
89
+ return config
90
+ }
91
+
92
+ return {
93
+ build: {
94
+ rollupOptions: {
95
+ input: {
96
+ [entry]: virtualPrefix
97
+ },
98
+ external: ['cloud'],
99
+ output: {
100
+ entryFileNames: '[name].js'
101
+ }
102
+ }
103
+ },
104
+ plugins: [react()]
105
+ }
106
+ },
107
+
108
+ resolveId: (id: string) => {
109
+ if (id === virtualPrefix) {
110
+ return id
111
+ }
112
+ return null
113
+ },
114
+
115
+ load: (id: string) => {
116
+ if (id === virtualPrefix) {
117
+ const script = `
118
+ import { StrictMode } from 'react'
119
+ import { createRoot } from 'react-dom/client'
120
+
121
+ import { Page } from "${filePath.replace(/\\/g, '\\\\')}";
122
+
123
+ createRoot(document.getElementById('root')!).render(
124
+ <StrictMode>
125
+ <Page />
126
+ </StrictMode>
127
+ )`
128
+
129
+ return script
130
+ }
131
+ return null
132
+ }
133
+ }
134
+ }
135
+
136
+ export const spaClientConfig = (filePath: string): InlineConfig => {
137
+ return {
138
+ logLevel: 'error',
139
+ plugins: [
140
+ jsSpa({
141
+ filePath
142
+ }),
143
+ tailwindcss()
144
+ ],
145
+ build: {
146
+ outDir: '/tmp'
147
+ }
148
+ }
149
+ }