@tothalex/nulljs 0.0.40
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 +45 -0
- package/scripts/install-server.js +132 -0
- package/src/commands/api.ts +16 -0
- package/src/commands/auth.ts +54 -0
- package/src/commands/create.ts +43 -0
- package/src/commands/deploy.ts +160 -0
- package/src/commands/dev/function/index.ts +221 -0
- package/src/commands/dev/function/utils.ts +99 -0
- package/src/commands/dev/index.tsx +126 -0
- package/src/commands/dev/logging-manager.ts +87 -0
- package/src/commands/dev/server/index.ts +48 -0
- package/src/commands/dev/server/utils.ts +37 -0
- package/src/commands/dev/ui/components/scroll-area.tsx +141 -0
- package/src/commands/dev/ui/components/tab-bar.tsx +67 -0
- package/src/commands/dev/ui/index.tsx +71 -0
- package/src/commands/dev/ui/logging-context.tsx +76 -0
- package/src/commands/dev/ui/tabs/functions-tab.tsx +35 -0
- package/src/commands/dev/ui/tabs/server-tab.tsx +36 -0
- package/src/commands/dev/ui/tabs/vite-tab.tsx +35 -0
- package/src/commands/dev/ui/use-logging.tsx +34 -0
- package/src/commands/dev/vite/index.ts +54 -0
- package/src/commands/dev/vite/utils.ts +71 -0
- package/src/commands/host.ts +339 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/profile.ts +189 -0
- package/src/commands/secret.ts +79 -0
- package/src/index.ts +346 -0
- package/src/lib/api.ts +189 -0
- package/src/lib/bundle/external.ts +23 -0
- package/src/lib/bundle/function/index.ts +46 -0
- package/src/lib/bundle/index.ts +2 -0
- package/src/lib/bundle/react/index.ts +2 -0
- package/src/lib/bundle/react/spa.ts +77 -0
- package/src/lib/bundle/react/ssr/client.ts +93 -0
- package/src/lib/bundle/react/ssr/config.ts +77 -0
- package/src/lib/bundle/react/ssr/index.ts +4 -0
- package/src/lib/bundle/react/ssr/props.ts +71 -0
- package/src/lib/bundle/react/ssr/server.ts +83 -0
- package/src/lib/bundle/types.ts +4 -0
- package/src/lib/config.ts +347 -0
- package/src/lib/deployment.ts +244 -0
- package/src/lib/update-server.ts +180 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tothalex/nulljs",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"version": "0.0.40",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nulljs": "./src/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "bun scripts/install-server.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"scripts"
|
|
15
|
+
],
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@eslint/js": "^9.20.0",
|
|
18
|
+
"@types/bun": "1.2.16",
|
|
19
|
+
"@types/degit": "^2.8.6",
|
|
20
|
+
"@types/mime-types": "^2.1.4",
|
|
21
|
+
"@types/yargs": "^17.0.33",
|
|
22
|
+
"eslint": "^9.20.1",
|
|
23
|
+
"rollup": "^4.30.1",
|
|
24
|
+
"typescript-eslint": "^8.24.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"typescript": "~5.7.2"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@tailwindcss/vite": "^4.1.11",
|
|
31
|
+
"@types/react": "^19.1.11",
|
|
32
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
33
|
+
"@vitejs/plugin-react-swc": "^3.8.0",
|
|
34
|
+
"chalk": "^5.4.1",
|
|
35
|
+
"chokidar": "^4.0.3",
|
|
36
|
+
"degit": "^2.8.4",
|
|
37
|
+
"execa": "^9.6.0",
|
|
38
|
+
"ink": "^6.2.2",
|
|
39
|
+
"mime-types": "^2.1.35",
|
|
40
|
+
"react": "^19.1.1",
|
|
41
|
+
"tar": "^7.5.2",
|
|
42
|
+
"vite": "^7.0.0",
|
|
43
|
+
"yargs": "^17.7.2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const https = require('https')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const { createWriteStream } = require('fs')
|
|
7
|
+
const tar = require('tar')
|
|
8
|
+
|
|
9
|
+
function getPlatformInfo() {
|
|
10
|
+
const platform = process.platform
|
|
11
|
+
const arch = process.arch
|
|
12
|
+
|
|
13
|
+
const platformMap = {
|
|
14
|
+
linux: {
|
|
15
|
+
x64: 'x86_64-unknown-linux-gnu',
|
|
16
|
+
arm64: 'aarch64-unknown-linux-gnu'
|
|
17
|
+
},
|
|
18
|
+
darwin: {
|
|
19
|
+
x64: 'x86_64-apple-darwin',
|
|
20
|
+
arm64: 'aarch64-apple-darwin'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const target = platformMap[platform]?.[arch]
|
|
25
|
+
if (!target) {
|
|
26
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const extension = '.tar.gz'
|
|
30
|
+
const binaryName = 'server'
|
|
31
|
+
|
|
32
|
+
return { target, extension, binaryName }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function downloadFile(url, destination) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const file = createWriteStream(destination)
|
|
38
|
+
|
|
39
|
+
https
|
|
40
|
+
.get(url, (response) => {
|
|
41
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
42
|
+
// Handle redirect
|
|
43
|
+
return downloadFile(response.headers.location, destination).then(resolve).catch(reject)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (response.statusCode !== 200) {
|
|
47
|
+
reject(new Error(`Failed to download: ${response.statusCode}`))
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
response.pipe(file)
|
|
52
|
+
|
|
53
|
+
file.on('finish', () => {
|
|
54
|
+
file.close()
|
|
55
|
+
resolve()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
file.on('error', reject)
|
|
59
|
+
})
|
|
60
|
+
.on('error', reject)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function extractArchive(archivePath, extractPath, binaryName) {
|
|
65
|
+
// Extract tar.gz
|
|
66
|
+
await tar.x({
|
|
67
|
+
file: archivePath,
|
|
68
|
+
cwd: extractPath
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Make binary executable
|
|
72
|
+
const binaryPath = path.join(extractPath, binaryName)
|
|
73
|
+
fs.chmodSync(binaryPath, '755')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function getLatestReleaseUrl(target, extension) {
|
|
77
|
+
const baseUrl = 'https://nulljs.s3.eu-north-1.amazonaws.com/releases'
|
|
78
|
+
return `${baseUrl}/nulljs-server-${target}/nulljs-server-${target}${extension}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function installServer() {
|
|
82
|
+
try {
|
|
83
|
+
console.log('📦 Installing nulljs server binary...')
|
|
84
|
+
|
|
85
|
+
const { target, extension, binaryName } = getPlatformInfo()
|
|
86
|
+
|
|
87
|
+
const binDir = path.join(__dirname, '..', 'bin')
|
|
88
|
+
const archivePath = path.join(binDir, `server${extension}`)
|
|
89
|
+
const binaryPath = path.join(binDir, 'server')
|
|
90
|
+
|
|
91
|
+
// Create bin directory
|
|
92
|
+
if (!fs.existsSync(binDir)) {
|
|
93
|
+
fs.mkdirSync(binDir, { recursive: true })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if binary already exists
|
|
97
|
+
if (fs.existsSync(binaryPath)) {
|
|
98
|
+
console.log('✅ Server binary already installed')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`🔍 Downloading server for ${target}...`)
|
|
103
|
+
|
|
104
|
+
// Get download URL from latest release
|
|
105
|
+
const downloadUrl = await getLatestReleaseUrl(target, extension)
|
|
106
|
+
|
|
107
|
+
// Download the archive
|
|
108
|
+
await downloadFile(downloadUrl, archivePath)
|
|
109
|
+
console.log('✅ Download completed')
|
|
110
|
+
|
|
111
|
+
// Extract the binary
|
|
112
|
+
console.log('📂 Extracting binary...')
|
|
113
|
+
await extractArchive(archivePath, binDir, binaryName)
|
|
114
|
+
|
|
115
|
+
// Clean up archive
|
|
116
|
+
fs.unlinkSync(archivePath)
|
|
117
|
+
|
|
118
|
+
console.log('✅ nulljs server installed successfully')
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('❌ Failed to install server binary:', error.message)
|
|
121
|
+
console.error('⚠️ You may need to build the server manually')
|
|
122
|
+
process.exit(0) // Don't fail the installation
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Only run if called directly
|
|
127
|
+
if (require.main === module) {
|
|
128
|
+
installServer()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { installServer }
|
|
132
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import { saveApiUrl } from '../lib/config'
|
|
4
|
+
|
|
5
|
+
const setApiUrl = (url: string, profileName?: string) => {
|
|
6
|
+
try {
|
|
7
|
+
saveApiUrl(url, profileName)
|
|
8
|
+
const profileText = profileName ? ` to profile '${profileName}'` : ''
|
|
9
|
+
console.log(chalk.green('✓'), `API URL saved${profileText}: ${chalk.blue(url)}`)
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(chalk.red('✗'), 'Failed to save API URL:', error)
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { setApiUrl }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import readline from 'node:readline'
|
|
3
|
+
|
|
4
|
+
import { loadConfigWithProfile, saveKeys } from '../lib/config'
|
|
5
|
+
|
|
6
|
+
const askQuestion = (query: string): Promise<string> => {
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
return new Promise((resolve) =>
|
|
13
|
+
rl.question(query, (answer) => {
|
|
14
|
+
rl.close()
|
|
15
|
+
resolve(answer.trim())
|
|
16
|
+
})
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const auth = async (profileName?: string) => {
|
|
21
|
+
const config = loadConfigWithProfile()
|
|
22
|
+
|
|
23
|
+
if (config.key) {
|
|
24
|
+
const answer = await askQuestion(
|
|
25
|
+
'Keys already exist. Are you sure you want to regenerate them? (Y/n): '
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if (answer.toLowerCase() === 'n') {
|
|
29
|
+
console.log(chalk.green('Public Key:'), chalk.blue(config.key.public))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
35
|
+
{
|
|
36
|
+
name: 'Ed25519',
|
|
37
|
+
namedCurve: 'Ed25519'
|
|
38
|
+
},
|
|
39
|
+
true,
|
|
40
|
+
['sign', 'verify']
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
|
|
44
|
+
const publicKeyBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey)
|
|
45
|
+
|
|
46
|
+
const privateKey = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)))
|
|
47
|
+
const publicKey = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)))
|
|
48
|
+
|
|
49
|
+
saveKeys(privateKey, publicKey, profileName)
|
|
50
|
+
|
|
51
|
+
console.log(chalk.green('Public Key:'), chalk.blue(publicKey))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { auth }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import degit from 'degit'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
const repo = 'tothalex/nulljs-template'
|
|
7
|
+
|
|
8
|
+
const modifyName = async (newName: string, packagePath: string) => {
|
|
9
|
+
const file = Bun.file(packagePath)
|
|
10
|
+
const packageContent = await file.text()
|
|
11
|
+
const packageJson = JSON.parse(packageContent)
|
|
12
|
+
|
|
13
|
+
packageJson.name = newName
|
|
14
|
+
|
|
15
|
+
await Bun.write(packagePath, JSON.stringify(packageJson, null, 2) + '\n')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const create = async (name: string) => {
|
|
19
|
+
if (existsSync(name)) {
|
|
20
|
+
console.log(chalk.red('Folder already exists: ') + chalk.bgRed(name))
|
|
21
|
+
process.exit(0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const targetDir = path.join(process.cwd(), name)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const emitter = degit(repo, {
|
|
28
|
+
cache: false,
|
|
29
|
+
force: true,
|
|
30
|
+
verbose: true
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await emitter.clone(targetDir)
|
|
34
|
+
|
|
35
|
+
await modifyName(name, targetDir + '/package.json')
|
|
36
|
+
console.log(chalk.green('Project setup completed successfully!'))
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(chalk.red('An error occurred during project creation:'))
|
|
39
|
+
console.error(error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { create }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { resolve } from 'path'
|
|
2
|
+
import { readdir, exists, stat } from 'node:fs/promises'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import chokidar from 'chokidar'
|
|
5
|
+
|
|
6
|
+
import { deployFunction, deployPage, isReact, isTypescript } from '../lib/deployment'
|
|
7
|
+
import { requireProfileConfiguration } from '../lib/config'
|
|
8
|
+
|
|
9
|
+
const deployFunctionsRecursively = async (functionsPath: string, force: boolean = true) => {
|
|
10
|
+
if (!(await exists(functionsPath))) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const functionsStat = await stat(functionsPath)
|
|
15
|
+
if (!functionsStat.isDirectory()) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const items = await readdir(functionsPath)
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
const fullPath = `${functionsPath}/${item}`
|
|
22
|
+
const itemStat = await stat(fullPath)
|
|
23
|
+
|
|
24
|
+
if (itemStat.isDirectory()) {
|
|
25
|
+
// Recursively process subdirectories
|
|
26
|
+
await deployFunctionsRecursively(fullPath, force)
|
|
27
|
+
} else if (isTypescript(item)) {
|
|
28
|
+
const result = await deployFunction(fullPath, console, force)
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
console.log(chalk.red(`${item} deployment failed: ${result.error.message}`))
|
|
32
|
+
} else if (result.cached) {
|
|
33
|
+
console.log(chalk.gray(`${item} skipped (no changes)`))
|
|
34
|
+
} else if (result.deployed) {
|
|
35
|
+
console.log(chalk.green(`${item} deployed successfully`))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const deploy = async (path?: string, force: boolean = true) => {
|
|
42
|
+
// Ensure a profile is selected and properly configured for deployment
|
|
43
|
+
const { profileName, config } = requireProfileConfiguration(['api', 'key'])
|
|
44
|
+
console.log(chalk.gray(`Using profile: ${profileName}`))
|
|
45
|
+
console.log(config.api ? chalk.gray(`API: ${config.api}`) : '')
|
|
46
|
+
console.log(config.key?.public ? chalk.gray(`Key: ${config.key.public}`) : '')
|
|
47
|
+
|
|
48
|
+
let deployPath = path
|
|
49
|
+
|
|
50
|
+
// If no path provided, look for index.tsx
|
|
51
|
+
if (!deployPath) {
|
|
52
|
+
const indexPath = resolve('src/index.tsx')
|
|
53
|
+
if (await exists(indexPath)) {
|
|
54
|
+
deployPath = indexPath
|
|
55
|
+
} else {
|
|
56
|
+
deployPath = resolve('.')
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!(await exists(deployPath))) {
|
|
61
|
+
console.log(chalk.red("Path doesn't exists: ") + chalk.bgRed(deployPath))
|
|
62
|
+
process.exit(0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pathStat = await stat(deployPath)
|
|
66
|
+
|
|
67
|
+
if (pathStat.isFile()) {
|
|
68
|
+
const file = resolve(deployPath)
|
|
69
|
+
|
|
70
|
+
if (isReact(file)) {
|
|
71
|
+
console.log(`${chalk.gray('Deploying page: ')} ${chalk.green(file)}`)
|
|
72
|
+
await deployPage(file)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Also deploy functions directory recursively
|
|
76
|
+
const functionsPath = resolve('src/function')
|
|
77
|
+
await deployFunctionsRecursively(functionsPath, force)
|
|
78
|
+
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const dir = await readdir(deployPath)
|
|
83
|
+
const p = resolve(deployPath)
|
|
84
|
+
for (const file of dir) {
|
|
85
|
+
if (isTypescript(file)) {
|
|
86
|
+
const result = await deployFunction(`${p}/${file}`, console, force)
|
|
87
|
+
|
|
88
|
+
if (result.error) {
|
|
89
|
+
console.log(chalk.red(`${file} deployment failed: ${result.error.message}`))
|
|
90
|
+
} else if (result.cached) {
|
|
91
|
+
console.log(chalk.gray(`${file} skipped (no changes)`))
|
|
92
|
+
} else if (result.deployed) {
|
|
93
|
+
console.log(chalk.green(`${file} deployed successfully`))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isReact(file)) {
|
|
98
|
+
await deployPage(`${p}/${file}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Also deploy functions directory recursively
|
|
103
|
+
const functionsPath = resolve('src/function')
|
|
104
|
+
await deployFunctionsRecursively(functionsPath, force)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const onFileChange = async (file: string) => {
|
|
108
|
+
const absolutePath = resolve(file)
|
|
109
|
+
console.log(`File changed: ${absolutePath}`)
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const indexPage = resolve('src/index.tsx')
|
|
113
|
+
if (await exists(indexPage)) {
|
|
114
|
+
if (isReact(indexPage)) {
|
|
115
|
+
await deployPage(indexPage)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Deploy all files in src/functions/ directory recursively
|
|
120
|
+
const functionsPath = resolve('src/function')
|
|
121
|
+
await deployFunctionsRecursively(functionsPath)
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(chalk.red('Deployment failed:'), error)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const watch = async (path: string) => {
|
|
128
|
+
// Ensure a profile is selected and properly configured for deployment
|
|
129
|
+
const { profileName } = requireProfileConfiguration(['api', 'key'])
|
|
130
|
+
console.log(chalk.gray(`Using profile: ${profileName}`))
|
|
131
|
+
|
|
132
|
+
if (!exists(path)) {
|
|
133
|
+
console.log(chalk.red("Path doesn't exists: ") + chalk.bgRed(path))
|
|
134
|
+
process.exit(0)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const watcher = chokidar.watch(`${path}`, {
|
|
138
|
+
ignored: (path) => ['node_modules', '.git'].some((s) => path.includes(s)),
|
|
139
|
+
persistent: true,
|
|
140
|
+
ignoreInitial: true,
|
|
141
|
+
cwd: process.cwd()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
watcher
|
|
145
|
+
.on('add', (filePath) => onFileChange(filePath))
|
|
146
|
+
.on('change', (filePath) => onFileChange(filePath))
|
|
147
|
+
.on('ready', () => {
|
|
148
|
+
console.log('Initial scan complete. Ready for changes')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
process.on('SIGINT', () => {
|
|
152
|
+
console.log('\nShutting down file watcher...')
|
|
153
|
+
watcher.close().then(() => {
|
|
154
|
+
console.log('File watcher stopped')
|
|
155
|
+
process.exit(0)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { deploy, watch }
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
import chokidar from 'chokidar'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import ts from 'typescript'
|
|
6
|
+
|
|
7
|
+
import { deployFunctionAndLog } from './utils'
|
|
8
|
+
|
|
9
|
+
// Track dependencies between files
|
|
10
|
+
const dependencyGraph = new Map<string, Set<string>>()
|
|
11
|
+
const reverseDependencyGraph = new Map<string, Set<string>>()
|
|
12
|
+
|
|
13
|
+
function analyzeDependencies(filePath: string): Set<string> {
|
|
14
|
+
const dependencies = new Set<string>()
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const fs = require('fs')
|
|
18
|
+
const path = require('path')
|
|
19
|
+
|
|
20
|
+
const sourceCode = fs.readFileSync(filePath, 'utf-8')
|
|
21
|
+
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true)
|
|
22
|
+
|
|
23
|
+
function visit(node: ts.Node) {
|
|
24
|
+
if (
|
|
25
|
+
ts.isImportDeclaration(node) &&
|
|
26
|
+
node.moduleSpecifier &&
|
|
27
|
+
ts.isStringLiteral(node.moduleSpecifier)
|
|
28
|
+
) {
|
|
29
|
+
const importPath = node.moduleSpecifier.text
|
|
30
|
+
|
|
31
|
+
// Resolve relative imports
|
|
32
|
+
if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
33
|
+
const resolvedPath = path.resolve(path.dirname(filePath), importPath)
|
|
34
|
+
|
|
35
|
+
// Try different extensions
|
|
36
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx']
|
|
37
|
+
for (const ext of extensions) {
|
|
38
|
+
const fullPath = resolvedPath + ext
|
|
39
|
+
if (fs.existsSync(fullPath)) {
|
|
40
|
+
dependencies.add(fullPath)
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Also try index files
|
|
46
|
+
const indexPath = path.join(resolvedPath, 'index.ts')
|
|
47
|
+
if (fs.existsSync(indexPath)) {
|
|
48
|
+
dependencies.add(indexPath)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ts.forEachChild(node, visit)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
visit(sourceFile)
|
|
57
|
+
} catch {
|
|
58
|
+
// Silent fallback - dependency tracking is nice-to-have
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return dependencies
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function updateDependencyGraph(filePath: string) {
|
|
65
|
+
// Remove old dependencies
|
|
66
|
+
const oldDeps = dependencyGraph.get(filePath) || new Set()
|
|
67
|
+
oldDeps.forEach((dep) => {
|
|
68
|
+
const reverseDeps = reverseDependencyGraph.get(dep) || new Set()
|
|
69
|
+
reverseDeps.delete(filePath)
|
|
70
|
+
if (reverseDeps.size === 0) {
|
|
71
|
+
reverseDependencyGraph.delete(dep)
|
|
72
|
+
} else {
|
|
73
|
+
reverseDependencyGraph.set(dep, reverseDeps)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Add new dependencies
|
|
78
|
+
const newDeps = analyzeDependencies(filePath)
|
|
79
|
+
dependencyGraph.set(filePath, newDeps)
|
|
80
|
+
|
|
81
|
+
newDeps.forEach((dep) => {
|
|
82
|
+
const reverseDeps = reverseDependencyGraph.get(dep) || new Set()
|
|
83
|
+
reverseDeps.add(filePath)
|
|
84
|
+
reverseDependencyGraph.set(dep, reverseDeps)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getAffectedFunctions(changedFile: string, functionDir: string): Set<string> {
|
|
89
|
+
const affected = new Set<string>()
|
|
90
|
+
|
|
91
|
+
// If the changed file is already in function dir, include it
|
|
92
|
+
if (changedFile.startsWith(functionDir) && changedFile.endsWith('.ts')) {
|
|
93
|
+
affected.add(changedFile)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Find all functions that depend on this changed file
|
|
97
|
+
function findDependents(filePath: string, visited = new Set<string>()) {
|
|
98
|
+
if (visited.has(filePath)) return
|
|
99
|
+
visited.add(filePath)
|
|
100
|
+
|
|
101
|
+
const dependents = reverseDependencyGraph.get(filePath) || new Set()
|
|
102
|
+
dependents.forEach((dependent) => {
|
|
103
|
+
if (dependent.startsWith(functionDir) && dependent.endsWith('.ts')) {
|
|
104
|
+
affected.add(dependent)
|
|
105
|
+
}
|
|
106
|
+
findDependents(dependent, visited)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
findDependents(changedFile)
|
|
111
|
+
return affected
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const createFunctionWatcher = (props: { srcDir: string; log: (data: string) => void }) => {
|
|
115
|
+
const functionDir = resolve(props.srcDir, 'function')
|
|
116
|
+
|
|
117
|
+
if (!existsSync(functionDir)) {
|
|
118
|
+
props.log(
|
|
119
|
+
chalk.yellow(
|
|
120
|
+
`⚠️ Functions directory not found at ${functionDir}, skipping function watcher.`
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
props.log(chalk.cyan(`👀 Watching functions and dependencies in ${props.srcDir}`))
|
|
127
|
+
|
|
128
|
+
// Build initial dependency graph for all TypeScript files
|
|
129
|
+
const buildInitialDependencyGraph = () => {
|
|
130
|
+
const glob = require('glob')
|
|
131
|
+
const tsFiles = glob.sync(`${props.srcDir}/**/*.ts`, {
|
|
132
|
+
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
props.log(chalk.gray(`🔍 Analyzing ${tsFiles.length} files for dependencies...`))
|
|
136
|
+
|
|
137
|
+
tsFiles.forEach((filePath: string) => {
|
|
138
|
+
const absolutePath = resolve(filePath)
|
|
139
|
+
updateDependencyGraph(absolutePath)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
props.log(chalk.gray(`✓ Ready for smart rebuilds`))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Build initial graph
|
|
146
|
+
buildInitialDependencyGraph()
|
|
147
|
+
|
|
148
|
+
return chokidar
|
|
149
|
+
.watch(props.srcDir, {
|
|
150
|
+
ignored: (path) => {
|
|
151
|
+
// Exclude common web/React directories and file types
|
|
152
|
+
const excludePatterns = [
|
|
153
|
+
'node_modules',
|
|
154
|
+
'.git',
|
|
155
|
+
'pages',
|
|
156
|
+
'components',
|
|
157
|
+
'styles',
|
|
158
|
+
'public',
|
|
159
|
+
'assets',
|
|
160
|
+
'dist',
|
|
161
|
+
'build'
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
// Exclude file extensions that aren't relevant to functions
|
|
165
|
+
const excludeExtensions = [
|
|
166
|
+
'.css',
|
|
167
|
+
'.scss',
|
|
168
|
+
'.sass',
|
|
169
|
+
'.less',
|
|
170
|
+
'.jsx',
|
|
171
|
+
'.tsx',
|
|
172
|
+
'.html',
|
|
173
|
+
'.md'
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
excludePatterns.some((pattern) => path.includes(pattern)) ||
|
|
178
|
+
excludeExtensions.some((ext) => path.endsWith(ext))
|
|
179
|
+
)
|
|
180
|
+
},
|
|
181
|
+
persistent: true,
|
|
182
|
+
ignoreInitial: true,
|
|
183
|
+
cwd: process.cwd()
|
|
184
|
+
})
|
|
185
|
+
.on('add', (filePath) => {
|
|
186
|
+
if (filePath.endsWith('.ts')) {
|
|
187
|
+
const absolutePath = resolve(filePath)
|
|
188
|
+
updateDependencyGraph(absolutePath)
|
|
189
|
+
|
|
190
|
+
// Only deploy if it's a function file
|
|
191
|
+
if (absolutePath.startsWith(functionDir)) {
|
|
192
|
+
deployFunctionAndLog(absolutePath, props.log)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
.on('change', (filePath) => {
|
|
197
|
+
if (filePath.endsWith('.ts')) {
|
|
198
|
+
const absolutePath = resolve(filePath)
|
|
199
|
+
updateDependencyGraph(absolutePath)
|
|
200
|
+
|
|
201
|
+
// Find all affected functions and deploy them
|
|
202
|
+
const affectedFunctions = getAffectedFunctions(absolutePath, functionDir)
|
|
203
|
+
|
|
204
|
+
if (affectedFunctions.size > 0) {
|
|
205
|
+
const fileName = require('path').basename(filePath)
|
|
206
|
+
const functionNames = Array.from(affectedFunctions).map((f) =>
|
|
207
|
+
require('path').basename(f, '.ts')
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
props.log(chalk.blue(`${fileName} changed → checking ${functionNames.join(', ')}`))
|
|
211
|
+
|
|
212
|
+
affectedFunctions.forEach((functionPath) => {
|
|
213
|
+
deployFunctionAndLog(functionPath, props.log)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.on('error', (error) => {
|
|
219
|
+
props.log(chalk.red('❌ Function watcher error:' + error))
|
|
220
|
+
})
|
|
221
|
+
}
|