@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/package.json +8 -5
- package/src/cli.ts +0 -24
- package/src/commands/config.ts +0 -130
- package/src/commands/deploy.ts +0 -219
- package/src/commands/dev.ts +0 -10
- package/src/commands/host.ts +0 -330
- package/src/commands/index.ts +0 -6
- package/src/commands/secret.ts +0 -387
- package/src/commands/status.ts +0 -41
- package/src/components/DeployAnimation.tsx +0 -92
- package/src/components/DeploymentLogsPane.tsx +0 -79
- package/src/components/Header.tsx +0 -57
- package/src/components/HelpModal.tsx +0 -64
- package/src/components/SystemLogsPane.tsx +0 -78
- package/src/config/index.ts +0 -181
- package/src/lib/bundle/external.ts +0 -23
- package/src/lib/bundle/function.ts +0 -125
- package/src/lib/bundle/index.ts +0 -5
- package/src/lib/bundle/react.ts +0 -149
- package/src/lib/bundle/types.ts +0 -4
- package/src/lib/deploy.ts +0 -103
- package/src/lib/server.ts +0 -160
- package/src/lib/vite.ts +0 -120
- package/src/lib/watcher.ts +0 -274
- package/src/ui.tsx +0 -363
- package/tsconfig.json +0 -30
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
|
-
}
|
package/src/lib/watcher.ts
DELETED
|
@@ -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
|
-
}
|