Skip to content
This repository was archived by the owner on Aug 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"commander": "^9.4.1",
"debounce-promise": "^3.1.2",
"devicescript-compiler": "*",
"faye-websocket": "^0.11.4",
"fs-extra": "^10.1.0"
}
}
14 changes: 13 additions & 1 deletion cli/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
compile,
jacdacDefaultSpecifications,
JacsDiagnostic,
DEVS_ASSEMBLY_FILE,
} from "devicescript-compiler"
import { CmdOptions } from "./command"
import { startDevTools } from "./devtools"

function jacsFactory() {
let d = require("devicescript-vm")
Expand All @@ -30,7 +32,7 @@ async function getHost(options: BuildOptions) {
const inst = options.noVerify ? undefined : await jacsFactory()
inst?.jacsInit()

const outdir = options.outDir || "./built"
const outdir = options.outDir
ensureDirSync(outdir)

const jacsHost = {
Expand Down Expand Up @@ -84,11 +86,18 @@ export interface BuildOptions extends CmdOptions {

export async function build(file: string, options: BuildOptions) {
file = file || "main.ts"
options = options || {}
options.outDir = options.outDir || "./built"
options.mainFileName = file

await buildOnce(file, options)
if (options.watch) await buildWatch(file, options)
}

async function buildWatch(file: string, options: BuildOptions) {
const bytecodeFile = join(options.outDir, DEVS_ASSEMBLY_FILE)

// start watch source file
console.log(`watching ${file}...`)
const work = debounce(
async () => {
Expand All @@ -99,6 +108,9 @@ async function buildWatch(file: string, options: BuildOptions) {
{ leading: true }
)
watch(file, work)

// start watching bytecode file
await startDevTools(bytecodeFile)
}

async function buildOnce(file: string, options: BuildOptions) {
Expand Down
12 changes: 9 additions & 3 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@

import { build } from "./build"
import pkg from "../package.json"
import { program } from "commander"

export async function mainCli() {
program
.name("DeviceScript")
.description("build and run DeviceScript program https://aka.ms/devicescript")
.description(
"build and run DeviceScript program https://aka.ms/devicescript"
)
.version(pkg.version)
.option("-v, --verbose", "more logging")

program
.command("build", { isDefault: true })
.description("build a DeviceScript file")
.option("-w, --watch", "watch file changes and rebuild automatically")
.option("-l, --library", "build library")
.option("--no-verify", "don't verify resulting bytecode")
.option("-o", "--out-dir", "output directory, default is 'built'")
.option("-w, --watch", "watch file changes and rebuild automatically")
.option("--internet", "allow connections from non-localhost")
.option(
"--localhost",
"use localhost:8000 instead of the internet dashboard"
)
.arguments("[file.ts]")
.action(build)

Expand Down
145 changes: 145 additions & 0 deletions cli/src/devtools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
const SENDER_FIELD = "__jacdac_sender"
/* eslint-disable @typescript-eslint/no-var-requires */
const WebSocket = require("faye-websocket")
import http from "http"
import https from "https"
import url from "url"
import net from "net"
import fs from "fs"

const log = console.log
const debug = console.debug
const error = console.error

function fetchProxy(localhost: boolean): Promise<string> {
const protocol = localhost ? http : https
const url = localhost
? "http://localhost:8000/devtools/proxy.html"
: "https://microsoft.github.io/jacdac-docs/devtools/proxy"
//debug(`fetch jacdac devtools proxy at ${url}`)
return new Promise<string>((resolve, reject) => {
protocol
.get(url, res => {
if (res.statusCode != 200)
reject(
new Error(`proxy download failed (${res.statusCode})`)
)
res.setEncoding("utf8")
let body = ""
res.on("data", data => (body += data))
res.on("end", () => {
body = body.replace(
/https:\/\/microsoft.github.io\/jacdac-docs\/dashboard/g,
localhost
? "http://localhost:8000/devicescript/"
: "https://microsoft.github.io/jacdac-docs/editors/devicescript/"
)
resolve(body)
})
res.on("error", reject)
})
.on("error", reject)
})
}

export async function startDevTools(
bytecodeFile: string,
options?: {
internet?: boolean
localhost?: boolean
}
) {
const { internet, localhost } = options || {}
const port = 8081
const tcpPort = 8082
const listenHost = internet ? undefined : "127.0.0.1"

log(`start dev tools for ${bytecodeFile}`)
log(` dashboard: http://localhost:${port}`)
log(` websocket: ws://localhost:${port}`)
log(` tcpsocket: tcp://localhost:${tcpPort}`)

// download proxy sources
const proxyHtml = await fetchProxy(localhost)

// start http server
const clients: WebSocket[] = []

// upload DeviceScript file is needed
const sendDeviceScript = () => {
const bytecode = fs.readFileSync(bytecodeFile)
debug(`refresh bytecode...`)
const msg = JSON.stringify({
type: "source",
channel: "devicescript",
bytecode: bytecode.toString("hex"),
})
clients.forEach(c => c.send(msg))
}

const server = http.createServer(function (req, res) {
const parsedUrl = url.parse(req.url)
const pathname = parsedUrl.pathname
if (pathname === "/") {
res.setHeader("Cache-control", "no-cache")
res.setHeader("Content-type", "text/html")
res.end(proxyHtml)
} else {
res.statusCode = 404
}
})
function removeClient(client: WebSocket) {
const i = clients.indexOf(client)
clients.splice(i, 1)
log(`client: disconnected (${clients.length} clients)`)
}
server.on("upgrade", (request, socket, body) => {
// is this a socket?
if (WebSocket.isWebSocket(request)) {
const client = new WebSocket(request, socket, body)
const sender = "ws" + Math.random()
let firstDeviceScript = false
// store sender id to deduped packet
client[SENDER_FIELD] = sender
clients.push(client)
log(`webclient: connected (${sender}, ${clients.length} clients)`)
client.on("message", (event: any) => {
const { data } = event
if (!firstDeviceScript && sendDeviceScript) {
firstDeviceScript = true
sendDeviceScript()
}
})
client.on("close", () => removeClient(client))
client.on("error", (ev: Error) => error(ev))
}
})

const tcpServer = net.createServer((client: any) => {
const sender = "tcp" + Math.random()
client[SENDER_FIELD] = sender
client.send = (pkt0: Buffer) => {
const pkt = new Uint8Array(pkt0)
const b = new Uint8Array(1 + pkt.length)
b[0] = pkt.length
b.set(pkt, 1)
try {
client.write(b)
} catch {
try {
client.end()
} catch {} // eslint-disable-line no-empty
}
}
clients.push(client)
log(`tcpclient: connected (${sender} ${clients.length} clients)`)
client.on("end", () => removeClient(client))
client.on("error", (ev: Error) => error(ev))
})

server.listen(port, listenHost)
tcpServer.listen(tcpPort, listenHost)

debug(`watch ${bytecodeFile}`)
fs.watch(bytecodeFile, sendDeviceScript)
}
6 changes: 4 additions & 2 deletions compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export const JD_SERIAL_MAX_PAYLOAD_SIZE = 236
export const CMD_GET_REG = 0x1000
export const CMD_SET_REG = 0x2000

export const DEVS_ASSEMBLY_FILE = "prog.jasm"

class Cell {
_index: number

Expand Down Expand Up @@ -2792,7 +2794,7 @@ class Program implements TopOpWriter {

// early assembly dump, in case serialization fails
if (this.numErrors == 0)
this.host.write("prog.jasm", this.getAssembly())
this.host.write(DEVS_ASSEMBLY_FILE, this.getAssembly())

const b = this.serialize()
const dbg: DebugInfo = {
Expand All @@ -2812,7 +2814,7 @@ class Program implements TopOpWriter {

// write assembly again
if (this.numErrors == 0)
this.host.write("prog.jasm", this.getAssembly())
this.host.write(DEVS_ASSEMBLY_FILE, this.getAssembly())

if (this.numErrors == 0) {
try {
Expand Down
22 changes: 14 additions & 8 deletions website/docs/Dev/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,10 @@ The command tool is named `devicescript` or `devsc` for short.

## Build

The `build` command compiles the DeviceScript files in the current folder, using the resolution rules in `tsconfig.json`.
The `build` command compiles a DeviceScript file (default is `main.ts`), using the resolution rules in `tsconfig.json`. It is the default command.

```bash
devsc build
```

This is the default command, so you can omit build.

```bash
devsc
devsc build main.ts
```

### watch
Expand All @@ -38,3 +32,15 @@ add `--watch`.
```bash
devsc build --watch
```

When the build is run in watch mode, it also opens a developer tool web server that allows
to execute the compiled program in a virtual device or physical devices. Follow the console
application instructions to open the web page.

#### --internet

To access the developer tools outside localhost, add `--internet`

```bash
devsc build --watch --internet
```
31 changes: 31 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ ext@^1.1.2:
dependencies:
type "^2.7.2"

faye-websocket@^0.11.4:
version "0.11.4"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==
dependencies:
websocket-driver ">=0.5.1"

fs-extra@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
Expand All @@ -242,6 +249,11 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==

http-parser-js@>=0.5.1:
version "0.5.8"
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==

is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
Expand Down Expand Up @@ -271,6 +283,11 @@ node-gyp-build@^4.3.0:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==

safe-buffer@>=5.1.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==

tstl@^2.0.7:
version "2.5.13"
resolved "https://registry.yarnpkg.com/tstl/-/tstl-2.5.13.tgz#a5a5d27b79a12767e46a08525b3e045c5cdb1180"
Expand Down Expand Up @@ -310,6 +327,20 @@ utf-8-validate@^5.0.2:
dependencies:
node-gyp-build "^4.3.0"

websocket-driver@>=0.5.1:
version "0.7.4"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
dependencies:
http-parser-js ">=0.5.1"
safe-buffer ">=5.1.0"
websocket-extensions ">=0.1.1"

websocket-extensions@>=0.1.1:
version "0.1.4"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==

websocket-polyfill@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz#7321ada0f5f17516290ba1cb587ac111b74ce6a5"
Expand Down