@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.
Files changed (42) hide show
  1. package/package.json +45 -0
  2. package/scripts/install-server.js +132 -0
  3. package/src/commands/api.ts +16 -0
  4. package/src/commands/auth.ts +54 -0
  5. package/src/commands/create.ts +43 -0
  6. package/src/commands/deploy.ts +160 -0
  7. package/src/commands/dev/function/index.ts +221 -0
  8. package/src/commands/dev/function/utils.ts +99 -0
  9. package/src/commands/dev/index.tsx +126 -0
  10. package/src/commands/dev/logging-manager.ts +87 -0
  11. package/src/commands/dev/server/index.ts +48 -0
  12. package/src/commands/dev/server/utils.ts +37 -0
  13. package/src/commands/dev/ui/components/scroll-area.tsx +141 -0
  14. package/src/commands/dev/ui/components/tab-bar.tsx +67 -0
  15. package/src/commands/dev/ui/index.tsx +71 -0
  16. package/src/commands/dev/ui/logging-context.tsx +76 -0
  17. package/src/commands/dev/ui/tabs/functions-tab.tsx +35 -0
  18. package/src/commands/dev/ui/tabs/server-tab.tsx +36 -0
  19. package/src/commands/dev/ui/tabs/vite-tab.tsx +35 -0
  20. package/src/commands/dev/ui/use-logging.tsx +34 -0
  21. package/src/commands/dev/vite/index.ts +54 -0
  22. package/src/commands/dev/vite/utils.ts +71 -0
  23. package/src/commands/host.ts +339 -0
  24. package/src/commands/index.ts +8 -0
  25. package/src/commands/profile.ts +189 -0
  26. package/src/commands/secret.ts +79 -0
  27. package/src/index.ts +346 -0
  28. package/src/lib/api.ts +189 -0
  29. package/src/lib/bundle/external.ts +23 -0
  30. package/src/lib/bundle/function/index.ts +46 -0
  31. package/src/lib/bundle/index.ts +2 -0
  32. package/src/lib/bundle/react/index.ts +2 -0
  33. package/src/lib/bundle/react/spa.ts +77 -0
  34. package/src/lib/bundle/react/ssr/client.ts +93 -0
  35. package/src/lib/bundle/react/ssr/config.ts +77 -0
  36. package/src/lib/bundle/react/ssr/index.ts +4 -0
  37. package/src/lib/bundle/react/ssr/props.ts +71 -0
  38. package/src/lib/bundle/react/ssr/server.ts +83 -0
  39. package/src/lib/bundle/types.ts +4 -0
  40. package/src/lib/config.ts +347 -0
  41. package/src/lib/deployment.ts +244 -0
  42. 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
+ }