Skip to content

Commit 0d4b915

Browse files
authored
Added support for running NodeJS Databricks apps locally (#3092)
## Changes Added support for running NodeJS Databricks apps locally ## Why This supports running NodeJS apps (in debug mode as well) locally in a similar way we do for Python apps by running ``` databricks apps run-local --prepare-environment --debug ``` ## Tests Added acceptance tests <!-- If your PR needs to be included in the release notes for next release, add a separate entry in NEXT_CHANGELOG.md as part of your PR. -->
1 parent 8d3a846 commit 0d4b915

File tree

13 files changed

+269
-28
lines changed

13 files changed

+269
-28
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const express = require('express');
2+
const app = express();
3+
const port = process.env.PORT || 8000;
4+
5+
// Root route
6+
app.get('/', (req, res) => {
7+
res.json({
8+
message: 'Hello From App',
9+
timestamp: new Date().toISOString(),
10+
status: 'running'
11+
});
12+
});
13+
14+
app.get('/shutdown', (req, res) => {
15+
console.log('Server closed')
16+
// Add a small delay to ensure response is sent before exit
17+
setTimeout(() => {
18+
process.exit(0);
19+
}, 1000);
20+
});
21+
22+
// Start the server
23+
app.listen(port, () => {
24+
console.log(`Server is running on port ${port}`);
25+
});
26+
27+
module.exports = app;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
command:
2+
- npm
3+
- run
4+
- run-app
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "app",
3+
"version": "1.0.0",
4+
"description": "A simple Node.js app",
5+
"main": "app.js",
6+
"scripts": {
7+
"run-app": "node app.js",
8+
"build": "echo 'Building app...'"
9+
},
10+
"dependencies": {
11+
"express": "^5.1.0"
12+
}
13+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
command:
2+
- node
3+
- -e
4+
- "console.log('Hello, world')"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Running command: node -e console.log('Hello, world')
2+
Hello, world
3+
4+
=== Starting the app in background...
5+
=== Waiting
6+
=== Checking app is running...
7+
>>> curl -s -o - http://127.0.0.1:$(port)
8+
{"message":"Hello From App","timestamp":"[TIMESTAMP]","status":"running"}
9+
10+
=== Sending shutdown request...
11+
>>> curl -s -o /dev/null http://127.0.0.1:$(port)/shutdown
12+
Process terminated
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
cd app
2+
3+
# We first run the command with different entry point which starts unblocking script
4+
# so we don't need to start it in background. It will install the dependencies as part of the command
5+
trace $CLI apps run-local --prepare-environment --entry-point test.yml 2>&1 | grep -w "Hello, world"
6+
7+
# Get 3 unique ports sequentially to avoid conflicts
8+
PORTS=$(allocate_ports.py 3 | tr -d '\r')
9+
10+
# Read ports into array
11+
PORTS_ARR=($(echo "$PORTS"))
12+
PORT="${PORTS_ARR[0]}"
13+
DEBUG_PORT="${PORTS_ARR[1]}"
14+
PROXY_PORT="${PORTS_ARR[2]}"
15+
16+
title "Starting the app in background..."
17+
trace $CLI apps run-local --prepare-environment --debug --port "$PROXY_PORT" --debug-port "$DEBUG_PORT" --app-port "$PORT" > ../out.run.txt 2>&1 &
18+
PID=$!
19+
# Ensure background process is killed on script exit
20+
trap '(kill $PID >/dev/null 2>&1) 2>/dev/null || true' EXIT
21+
cd ..
22+
23+
title Waiting for the app to start...
24+
# Use a loop to check for the startup message instead of tail/sed which can be unreliable on Windows
25+
# due to file locking, buffering issues, and different text processing behavior across Windows versions.
26+
# A simple grep loop is more robust across platforms.
27+
while [ -z "$(grep -o "Server is running on port " out.run.txt 2>/dev/null)" ]; do
28+
sleep 1
29+
done
30+
31+
title "Checking app is running..."
32+
trace curl -s -o - http://127.0.0.1:$PROXY_PORT | grep -w "Hello From App"
33+
34+
title "Sending shutdown request..."
35+
trace curl -s -o /dev/null http://127.0.0.1:$PROXY_PORT/shutdown || true
36+
37+
# We need to wait for the app to shutdown before we can exit the test meaning wait until the
38+
# server is closed. We need to poll because the server is closed asynchronously.
39+
while [ -z "$(grep -o "Server closed" out.run.txt 2>/dev/null)" ]; do
40+
sleep 1
41+
done
42+
43+
# Wait for the background process to actually terminate
44+
wait $PID 2>/dev/null || true
45+
echo "Process terminated"
46+
47+
rm out.run.txt
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
RecordRequests = false
2+
Timeout = '2m'
3+
TimeoutWindows = '10m'
4+
5+
Ignore = [
6+
'node_modules',
7+
'package-lock.json'
8+
]
9+
10+
[[Repls]]
11+
Old='curl/[0-9]+\.[0-9]+\.[0-9]+'
12+
New='curl/(version)'
13+
14+
[[Repls]]
15+
Old='127.0.0.1:[0-9]+'
16+
New='127.0.0.1:$(port)'
17+
18+
[[Repls]]
19+
Old='To debug your app, attach a debugger to port [0-9]+'
20+
New='To debug your app, attach a debugger to port $(debug_port)'

cmd/workspace/apps/run_local.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ func setupWorkspaceAndConfig(cmd *cobra.Command, entryPoint string, appPort int)
5353
func setupApp(cmd *cobra.Command, config *apps.Config, spec *apps.AppSpec, customEnv []string, prepareEnvironment bool) (apps.App, []string, error) {
5454
ctx := cmd.Context()
5555
cfg := cmdctx.ConfigUsed(ctx)
56-
app := apps.NewApp(ctx, config, spec)
56+
app, err := apps.NewApp(ctx, config, spec)
57+
if err != nil {
58+
return nil, nil, err
59+
}
60+
5761
env := auth.ProcessEnv(cfg)
5862
if cfg.Profile != "" {
5963
env = append(env, "DATABRICKS_CONFIG_PROFILE="+cfg.Profile)

libs/apps/apps.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
package apps
22

3-
import "context"
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
)
48

59
type App interface {
610
PrepareEnvironment() error
711
GetCommand(bool) ([]string, error)
812
}
913

10-
func NewApp(ctx context.Context, config *Config, spec *AppSpec) App {
11-
// We only support python apps for now, but later we can add more types
12-
// based on AppSpec
13-
return NewPythonApp(ctx, config, spec)
14+
func NewApp(ctx context.Context, config *Config, spec *AppSpec) (App, error) {
15+
// Check if the app is a Node.js app by checking if there is a package.json file in the root of the app
16+
packageJsonPath := filepath.Join(config.AppPath, "package.json")
17+
_, err := os.Stat(packageJsonPath)
18+
if err == nil {
19+
// Read the package.json file
20+
packageJson, err := readPackageJson(packageJsonPath)
21+
if err != nil {
22+
return nil, err
23+
}
24+
return NewNodeApp(ctx, config, spec, packageJson), nil
25+
}
26+
27+
return NewPythonApp(ctx, config, spec), nil
1428
}

0 commit comments

Comments
 (0)