@tothalex/nulljs 0.0.53 → 0.0.54

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/src/lib/deploy.ts DELETED
@@ -1,103 +0,0 @@
1
- import { basename } from 'path'
2
- import { build, type Rollup } from 'vite'
3
- import { createFunctionDeployment, createReactDeployment } from '@nulljs/api'
4
-
5
- import { functionConfig, spaClientConfig, spaConfigConfig } from './bundle'
6
-
7
- // Cache of file path -> hash of last deployed bundle
8
- const deployedHashes = new Map<string, string>()
9
-
10
- export const clearDeployCache = () => {
11
- deployedHashes.clear()
12
- }
13
-
14
- const hashCode = (code: string): string => {
15
- return Bun.hash(code).toString(16)
16
- }
17
-
18
- export type DeployResult = {
19
- deployed: boolean
20
- skipped: boolean
21
- }
22
-
23
- export const deployFunction = async (
24
- filePath: string,
25
- privateKey: CryptoKey,
26
- apiUrl: string
27
- ): Promise<DeployResult> => {
28
- const fileName = basename(filePath)
29
-
30
- const result = (await build(functionConfig(filePath))) as Rollup.RollupOutput
31
-
32
- const handler = result.output.find(
33
- (output): output is Rollup.OutputChunk =>
34
- output.type === 'chunk' && output.fileName === 'handler.js'
35
- )
36
-
37
- if (!handler) {
38
- throw new Error('Bundle failed: handler.js not found in output')
39
- }
40
-
41
- const hash = hashCode(handler.code)
42
- const lastHash = deployedHashes.get(filePath)
43
-
44
- if (lastHash === hash) {
45
- return { deployed: false, skipped: true }
46
- }
47
-
48
- await createFunctionDeployment(
49
- { name: fileName, assets: [{ fileName: 'handler.js', code: handler.code }] },
50
- { privateKey, url: apiUrl }
51
- )
52
-
53
- deployedHashes.set(filePath, hash)
54
- return { deployed: true, skipped: false }
55
- }
56
-
57
- export const deployReact = async (
58
- filePath: string,
59
- privateKey: CryptoKey,
60
- apiUrl: string
61
- ): Promise<DeployResult> => {
62
- const fileName = basename(filePath)
63
-
64
- // Build config.js file first
65
- const configResult = (await build(spaConfigConfig(filePath))) as Rollup.RollupOutput
66
- const configOutput = configResult.output.find(
67
- (output): output is Rollup.OutputChunk =>
68
- output.type === 'chunk' && output.fileName === 'index.js'
69
- )
70
-
71
- if (!configOutput) {
72
- throw new Error('Config build failed: config file not found in output')
73
- }
74
-
75
- // Build client assets
76
- const result = (await build(spaClientConfig(filePath))) as Rollup.RollupOutput
77
- const assets: Array<{ fileName: string; code: string }> = []
78
-
79
- // Add config.js first (required by backend)
80
- assets.push({ fileName: 'config.js', code: configOutput.code })
81
-
82
- for (const output of result.output) {
83
- if (output.type === 'chunk') {
84
- assets.push({ fileName: output.fileName, code: output.code })
85
- } else if (output.type === 'asset' && typeof output.source === 'string') {
86
- assets.push({ fileName: output.fileName, code: output.source })
87
- }
88
- }
89
-
90
- // Hash all assets combined
91
- const combinedCode = assets.map((a) => a.code).join('')
92
- const hash = hashCode(combinedCode)
93
- const lastHash = deployedHashes.get(filePath)
94
-
95
- if (lastHash === hash) {
96
- return { deployed: false, skipped: true }
97
- }
98
-
99
- await createReactDeployment({ name: fileName, assets }, { privateKey, url: apiUrl })
100
-
101
- deployedHashes.set(filePath, hash)
102
- return { deployed: true, skipped: false }
103
- }
package/src/lib/server.ts DELETED
@@ -1,160 +0,0 @@
1
- import { existsSync, mkdirSync, realpathSync } from 'fs'
2
- import { join, dirname } from 'path'
3
- import chalk from 'chalk'
4
- import { getOrCreateLocalDevConfig } from '../config'
5
-
6
- const CLI_DIR = dirname(realpathSync(import.meta.dir))
7
-
8
- type PlatformKey = 'linux-x64' | 'linux-arm64' | 'darwin-arm64'
9
-
10
- export type BinarySource = 'local-debug' | 'local-release' | 'local-env' | 'npm'
11
-
12
- export type ServerInfo = {
13
- process: ReturnType<typeof Bun.spawn>
14
- binarySource: BinarySource
15
- binaryPath: string
16
- }
17
-
18
- const PLATFORMS: Record<PlatformKey, string> = {
19
- 'linux-x64': '@tothalex/nulljs-linux-x64',
20
- 'linux-arm64': '@tothalex/nulljs-linux-arm64',
21
- 'darwin-arm64': '@tothalex/nulljs-darwin-arm64'
22
- }
23
-
24
- const getPlatformKey = (): string => {
25
- return `${process.platform}-${process.arch}`
26
- }
27
-
28
- const findProjectRoot = (startPath: string = process.cwd()): string => {
29
- let currentPath = startPath
30
- while (currentPath !== dirname(currentPath)) {
31
- if (existsSync(join(currentPath, 'package.json'))) {
32
- return currentPath
33
- }
34
- currentPath = dirname(currentPath)
35
- }
36
- return startPath
37
- }
38
-
39
- const findMonorepoRoot = (startPath: string = process.cwd()): string | null => {
40
- let currentPath = startPath
41
- while (currentPath !== dirname(currentPath)) {
42
- if (
43
- existsSync(join(currentPath, 'Cargo.toml')) &&
44
- existsSync(join(currentPath, 'package.json'))
45
- ) {
46
- return currentPath
47
- }
48
- currentPath = dirname(currentPath)
49
- }
50
- return null
51
- }
52
-
53
- const getLocalBinaryPath = (): { path: string; source: BinarySource } | null => {
54
- const envBinary = process.env.NULLJS_SERVER_BINARY
55
- if (envBinary && existsSync(envBinary)) {
56
- return { path: envBinary, source: 'local-env' }
57
- }
58
-
59
- // Check from cwd first, then from CLI package location (for bun link)
60
- const monorepoRoot = findMonorepoRoot() || findMonorepoRoot(CLI_DIR)
61
- if (monorepoRoot) {
62
- const debugBinary = join(monorepoRoot, 'target', 'debug', 'server')
63
- if (existsSync(debugBinary)) {
64
- return { path: debugBinary, source: 'local-debug' }
65
- }
66
- const releaseBinary = join(monorepoRoot, 'target', 'release', 'server')
67
- if (existsSync(releaseBinary)) {
68
- return { path: releaseBinary, source: 'local-release' }
69
- }
70
- }
71
-
72
- return null
73
- }
74
-
75
- // Removed getCloudPath as we now use unique paths per project
76
-
77
- const waitForServer = async (port: number, maxRetries = 30, delayMs = 1000): Promise<void> => {
78
- for (let i = 0; i < maxRetries; i++) {
79
- try {
80
- const response = await fetch(`http://localhost:${port}/api/health`)
81
- if (response.ok) {
82
- return
83
- }
84
- } catch {}
85
- await new Promise((resolve) => setTimeout(resolve, delayMs))
86
- }
87
- throw new Error(`Server did not start within ${(maxRetries * delayMs) / 1000}s`)
88
- }
89
-
90
- const buildDevArgs = async (): Promise<string[]> => {
91
- // Use project-local config (creates one if it doesn't exist)
92
- const devConfig = await getOrCreateLocalDevConfig()
93
-
94
- const args = ['--dev']
95
- args.push('--api-port', '3000')
96
- args.push('--gateway-port', '3001')
97
-
98
- // Use the project's .nulljs directory for cloud storage
99
- const projectRoot = findProjectRoot()
100
- const cloudPath = join(projectRoot, '.nulljs')
101
- args.push('--cloud-path', cloudPath)
102
-
103
- if (!existsSync(cloudPath)) {
104
- mkdirSync(cloudPath, { recursive: true })
105
- }
106
-
107
- args.push('--public-key', devConfig.key.public)
108
-
109
- return args
110
- }
111
-
112
- export const startServer = async (): Promise<ServerInfo> => {
113
- const localBinary = getLocalBinaryPath()
114
- let binaryPath: string
115
- let binarySource: BinarySource
116
-
117
- if (localBinary) {
118
- console.log(chalk.cyan(`Using local binary: ${localBinary.path}`))
119
- binaryPath = localBinary.path
120
- binarySource = localBinary.source
121
- } else {
122
- const platformKey = getPlatformKey()
123
- const pkgName = PLATFORMS[platformKey as PlatformKey]
124
-
125
- if (!pkgName) {
126
- console.error(chalk.red(`✗ Unsupported platform: ${platformKey}`))
127
- throw new Error(`Unsupported platform: ${platformKey}`)
128
- }
129
-
130
- binaryPath = require.resolve(`${pkgName}/bin/server`)
131
- binarySource = 'npm'
132
- console.log(chalk.cyan(`Using npm binary: ${pkgName}`))
133
- }
134
-
135
- try {
136
- const args = await buildDevArgs()
137
-
138
- console.log(chalk.yellow(`Starting server with args: ${args.join(' ')}`))
139
-
140
- const proc = Bun.spawn([binaryPath, ...args], {
141
- stdout: 'ignore',
142
- stderr: 'ignore'
143
- })
144
-
145
- console.log(chalk.green('✓ Server process started, waiting for server to be ready...'))
146
-
147
- await waitForServer(3000)
148
-
149
- console.log(chalk.green('✓ Server is ready'))
150
-
151
- return { process: proc, binarySource, binaryPath }
152
- } catch (error) {
153
- console.error(
154
- chalk.red(
155
- `✗ Failed to start server: ${error instanceof Error ? error.message : String(error)}`
156
- )
157
- )
158
- throw error
159
- }
160
- }
package/src/lib/vite.ts DELETED
@@ -1,120 +0,0 @@
1
- import { createServer, type ViteDevServer, type Logger } from 'vite'
2
- import { dirname, resolve, basename } from 'path'
3
- import react from '@vitejs/plugin-react'
4
- import tailwindcss from '@tailwindcss/vite'
5
- import { writeFile, unlink } from 'node:fs/promises'
6
- import { existsSync } from 'node:fs'
7
-
8
- const VITE_PORT = 5173
9
- const GATEWAY_PORT = 3001
10
-
11
- type ViteServerInfo = {
12
- server: ViteDevServer
13
- indexHtmlPath: string
14
- }
15
-
16
- const createViteLogger = (onLog?: (msg: string, level: string) => void): Logger => {
17
- return {
18
- info: (msg) => onLog?.(msg, 'info'),
19
- warn: (msg) => onLog?.(msg, 'warn'),
20
- warnOnce: (msg) => onLog?.(msg, 'warn'),
21
- error: (msg) => onLog?.(msg, 'error'),
22
- clearScreen: () => {},
23
- hasErrorLogged: () => false,
24
- hasWarned: false
25
- }
26
- }
27
-
28
- const createTempIndexHtml = (componentPath: string, projectRoot: string): string => {
29
- const relativePath = resolve(componentPath).replace(projectRoot + '/', '')
30
- const componentName = basename(componentPath, '.tsx')
31
-
32
- return `<!DOCTYPE html>
33
- <html lang="en">
34
- <head>
35
- <meta charset="UTF-8" />
36
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
37
- <title>${componentName} - Dev Mode</title>
38
- <style>
39
- * { box-sizing: border-box; }
40
- body { margin: 0; padding: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
41
- #root { min-height: 100vh; }
42
- </style>
43
- </head>
44
- <body>
45
- <div id="root"></div>
46
- <script type="module">
47
- import React from 'react'
48
- import ReactDOM from 'react-dom/client'
49
- import { Page } from '/${relativePath}'
50
-
51
- const root = ReactDOM.createRoot(document.getElementById('root'))
52
- root.render(
53
- React.createElement(React.StrictMode, null,
54
- React.createElement(Page)
55
- )
56
- )
57
- </script>
58
- </body>
59
- </html>`
60
- }
61
-
62
- export const startViteServer = async (options: {
63
- srcDir: string
64
- onLog?: (msg: string, level: string) => void
65
- }): Promise<ViteServerInfo | null> => {
66
- const indexTsxPath = resolve(options.srcDir, 'index.tsx')
67
-
68
- if (!existsSync(indexTsxPath)) {
69
- return null
70
- }
71
-
72
- const projectDir = dirname(options.srcDir)
73
- const tempIndexPath = resolve(projectDir, 'index.html')
74
-
75
- try {
76
- const indexHtml = createTempIndexHtml(indexTsxPath, projectDir)
77
- await writeFile(tempIndexPath, indexHtml)
78
-
79
- const server = await createServer({
80
- root: projectDir,
81
- plugins: [react(), tailwindcss()],
82
- customLogger: createViteLogger(options.onLog),
83
- server: {
84
- port: VITE_PORT,
85
- host: true,
86
- proxy: {
87
- '/api': {
88
- target: `http://localhost:${GATEWAY_PORT}`,
89
- changeOrigin: true,
90
- secure: false
91
- },
92
- '/assets': {
93
- target: `http://localhost:${GATEWAY_PORT}`,
94
- changeOrigin: true,
95
- secure: false
96
- }
97
- }
98
- }
99
- })
100
-
101
- await server.listen()
102
- options.onLog?.(`Vite server started on http://localhost:${VITE_PORT}`, 'info')
103
-
104
- return { server, indexHtmlPath: tempIndexPath }
105
- } catch (error) {
106
- options.onLog?.(`Failed to start Vite: ${error}`, 'error')
107
- return null
108
- }
109
- }
110
-
111
- export const stopViteServer = async (info: ViteServerInfo): Promise<void> => {
112
- await info.server.close()
113
-
114
- // Clean up temp index.html
115
- try {
116
- await unlink(info.indexHtmlPath)
117
- } catch {
118
- // Ignore if file doesn't exist
119
- }
120
- }
@@ -1,274 +0,0 @@
1
- import { watch, existsSync } from 'fs'
2
- import { readdir } from 'fs/promises'
3
- import { join } from 'path'
4
- import { build, type Rollup } from 'vite'
5
-
6
- import { readLocalConfig, loadPrivateKey } from '../config'
7
- import { functionWatchConfig, type FunctionEntry, type BuildResult } from './bundle'
8
- import { createFunctionDeployment } from '@nulljs/api'
9
-
10
- export type DeployResult = {
11
- name: string
12
- success: boolean
13
- skipped: boolean
14
- error?: string
15
- }
16
-
17
- export type WatcherOptions = {
18
- silent?: boolean
19
- onDeploy?: (filePath: string, success: boolean, skipped: boolean, error?: string) => void
20
- onDeployBatch?: (results: DeployResult[]) => void
21
- onReady?: (functionCount: number) => void
22
- }
23
-
24
- const FUNCTION_DIRS = ['api', 'cron', 'event'] as const
25
-
26
- // Cache of entry name -> hash of last deployed bundle
27
- const deployedHashes = new Map<string, string>()
28
-
29
- const hashCode = (code: string): string => {
30
- return Bun.hash(code).toString(16)
31
- }
32
-
33
- const findFunctionEntries = async (functionsPath: string): Promise<FunctionEntry[]> => {
34
- const entries: FunctionEntry[] = []
35
-
36
- for (const dir of FUNCTION_DIRS) {
37
- const dirPath = join(functionsPath, dir)
38
-
39
- try {
40
- const items = await readdir(dirPath, { withFileTypes: true })
41
-
42
- for (const item of items) {
43
- if (item.isFile() && (item.name.endsWith('.ts') || item.name.endsWith('.tsx'))) {
44
- const fullPath = join(dirPath, item.name)
45
- const name = `${dir}/${item.name.replace(/\.tsx?$/, '')}`
46
-
47
- entries.push({
48
- name,
49
- path: fullPath,
50
- type: dir
51
- })
52
- }
53
- }
54
- } catch {
55
- // Directory doesn't exist, skip
56
- }
57
- }
58
-
59
- return entries
60
- }
61
-
62
- export const startWatcher = async (
63
- srcPath: string,
64
- options: WatcherOptions = {}
65
- ): Promise<() => void> => {
66
- const { silent = false, onDeploy, onDeployBatch, onReady } = options
67
- const config = readLocalConfig()
68
-
69
- if (!config) {
70
- if (!silent) console.error('No local configuration found. Run "nulljs dev" first.')
71
- return () => {}
72
- }
73
-
74
- const functionsPath = join(srcPath, 'function')
75
- let entries = await findFunctionEntries(functionsPath)
76
- let viteWatcher: Rollup.RollupWatcher | null = null
77
- let isRestarting = false
78
-
79
- onReady?.(entries.length)
80
-
81
- const privateKey = await loadPrivateKey(config)
82
-
83
- const deployBatch = async (builds: BuildResult[]): Promise<DeployResult[]> => {
84
- const results: DeployResult[] = []
85
-
86
- for (const { entry, code } of builds) {
87
- const hash = hashCode(code)
88
- const lastHash = deployedHashes.get(entry.name)
89
-
90
- if (lastHash === hash) {
91
- // Skip unchanged - don't include in results
92
- continue
93
- }
94
-
95
- try {
96
- await createFunctionDeployment(
97
- {
98
- name: `${entry.name}.ts`,
99
- assets: [{ fileName: 'handler.js', code }]
100
- },
101
- { privateKey, url: config.api }
102
- )
103
- deployedHashes.set(entry.name, hash)
104
- results.push({ name: entry.name, success: true, skipped: false })
105
- } catch (error) {
106
- results.push({
107
- name: entry.name,
108
- success: false,
109
- skipped: false,
110
- error: error instanceof Error ? error.message : String(error)
111
- })
112
- }
113
- }
114
-
115
- return results
116
- }
117
-
118
- const startViteWatcher = async () => {
119
- if (entries.length === 0) {
120
- return null
121
- }
122
-
123
- const result = await build(
124
- functionWatchConfig(entries, {
125
- onBuildComplete: async (builds) => {
126
- const results = await deployBatch(builds)
127
-
128
- // Only notify if there were actual deploys
129
- if (results.length > 0) {
130
- onDeployBatch?.(results)
131
-
132
- // Also call individual onDeploy for backwards compatibility
133
- for (const r of results) {
134
- onDeploy?.(r.name, r.success, r.skipped, r.error)
135
- }
136
- }
137
- }
138
- })
139
- )
140
-
141
- // In watch mode, build() returns a RollupWatcher
142
- if ('on' in result) {
143
- return result as Rollup.RollupWatcher
144
- }
145
-
146
- return null
147
- }
148
-
149
- const restartViteWatcher = async () => {
150
- if (isRestarting) return
151
- isRestarting = true
152
-
153
- try {
154
- if (viteWatcher) {
155
- await viteWatcher.close()
156
- viteWatcher = null
157
- }
158
-
159
- entries = await findFunctionEntries(functionsPath)
160
- onReady?.(entries.length)
161
-
162
- viteWatcher = await startViteWatcher()
163
- } finally {
164
- isRestarting = false
165
- }
166
- }
167
-
168
- // Start initial Vite watcher
169
- viteWatcher = await startViteWatcher()
170
-
171
- // Watch function directories for new files (additions/deletions)
172
- const dirWatchers: ReturnType<typeof watch>[] = []
173
-
174
- for (const dir of FUNCTION_DIRS) {
175
- const dirPath = join(functionsPath, dir)
176
-
177
- if (!existsSync(dirPath)) continue
178
-
179
- try {
180
- const watcher = watch(dirPath, async (eventType, filename) => {
181
- if (!filename) return
182
- if (!filename.endsWith('.ts') && !filename.endsWith('.tsx')) return
183
-
184
- // Check if this is a new or deleted file
185
- const fullPath = join(dirPath, filename)
186
- const entryName = `${dir}/${filename.replace(/\.tsx?$/, '')}`
187
- const existingEntry = entries.find((e) => e.name === entryName)
188
- const fileExists = existsSync(fullPath)
189
-
190
- if ((!existingEntry && fileExists) || (existingEntry && !fileExists)) {
191
- // New file added or file deleted - restart Vite watcher
192
- await restartViteWatcher()
193
- }
194
- })
195
-
196
- dirWatchers.push(watcher)
197
- } catch {
198
- // Directory can't be watched
199
- }
200
- }
201
-
202
- // Return cleanup function
203
- return () => {
204
- for (const watcher of dirWatchers) {
205
- watcher.close()
206
- }
207
- if (viteWatcher) {
208
- viteWatcher.close()
209
- }
210
- }
211
- }
212
-
213
- export type ForceDeployOptions = {
214
- onDeploy?: (filePath: string, success: boolean, error?: string) => void
215
- onComplete?: (total: number, successful: number, failed: number) => void
216
- }
217
-
218
- export const forceDeployAll = async (
219
- srcPath: string,
220
- options: ForceDeployOptions = {}
221
- ): Promise<void> => {
222
- const { onDeploy, onComplete } = options
223
- const config = readLocalConfig()
224
-
225
- if (!config) {
226
- throw new Error('No local configuration found. Run "nulljs dev" first.')
227
- }
228
-
229
- const privateKey = await loadPrivateKey(config)
230
-
231
- // Clear the deploy cache to force re-deploy
232
- deployedHashes.clear()
233
-
234
- const functionsPath = join(srcPath, 'function')
235
- const entries = await findFunctionEntries(functionsPath)
236
-
237
- // React entrypoint is always src/index.tsx
238
- const reactEntry = join(srcPath, 'index.tsx')
239
- const hasReact = existsSync(reactEntry)
240
-
241
- let total = entries.length + (hasReact ? 1 : 0)
242
- let successful = 0
243
- let failed = 0
244
-
245
- // Deploy all functions using the existing single-file deploy
246
- const { deployFunction, deployReact, clearDeployCache } = await import('./deploy')
247
- clearDeployCache()
248
-
249
- for (const entry of entries) {
250
- try {
251
- await deployFunction(entry.path, privateKey, config.api)
252
- successful++
253
- onDeploy?.(entry.name, true)
254
- } catch (error) {
255
- failed++
256
- onDeploy?.(entry.name, false, error instanceof Error ? error.message : String(error))
257
- }
258
- }
259
-
260
- // Deploy React app (src/index.tsx)
261
- if (hasReact) {
262
- const fileName = 'index.tsx'
263
- try {
264
- await deployReact(reactEntry, privateKey, config.api)
265
- successful++
266
- onDeploy?.(fileName, true)
267
- } catch (error) {
268
- failed++
269
- onDeploy?.(fileName, false, error instanceof Error ? error.message : String(error))
270
- }
271
- }
272
-
273
- onComplete?.(total, successful, failed)
274
- }