Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
85d4bfb
feat: Introduce moddable adapter
nakasyou Jul 15, 2025
f633b48
format
nakasyou Jul 15, 2025
c59689d
format code
nakasyou Jul 15, 2025
14c6f2c
fix: delete deps to typings
nakasyou Jul 15, 2025
33f73a2
use ts-expect-error
nakasyou Jul 15, 2025
df2f4bb
fix
nakasyou Jul 15, 2025
fe340f7
fix for lint
nakasyou Jul 15, 2025
7a2c83f
actions
nakasyou Jul 15, 2025
aa54d53
use sudo
nakasyou Jul 15, 2025
655540f
fix path
nakasyou Jul 15, 2025
bb13b28
run
nakasyou Jul 15, 2025
4a3449c
use xs-dev
nakasyou Jul 15, 2025
51dec77
install node-gyp-build:
nakasyou Jul 15, 2025
963f6e8
fix
nakasyou Jul 15, 2025
b304ed7
vitest
nakasyou Jul 15, 2025
ab6566c
fix
nakasyou Jul 15, 2025
8dad36a
timeout
nakasyou Jul 15, 2025
8575cac
fix
nakasyou Jul 15, 2025
c33cce1
add test
nakasyou Jul 16, 2025
7423b4d
use path
nakasyou Jul 16, 2025
84ef5b0
fix: upload artiact
nakasyou Jul 16, 2025
1d7b6ec
fix: upload artiact
nakasyou Jul 16, 2025
ff21e9e
test
nakasyou Jul 16, 2025
fc2a1cb
test
nakasyou Jul 16, 2025
054cc65
test
nakasyou Jul 16, 2025
ee50eb0
test
nakasyou Jul 16, 2025
3de558c
xsbug-log
nakasyou Jul 16, 2025
3e5a43b
xsbug-log
nakasyou Jul 16, 2025
000fc93
xsbug-log
nakasyou Jul 16, 2025
042610c
xsbug-log
nakasyou Jul 16, 2025
2045eec
give up to create test
nakasyou Jul 16, 2025
792c3c5
fmt
nakasyou Jul 16, 2025
f2d80f0
fmt
nakasyou Jul 16, 2025
947bb0c
fmt
nakasyou Jul 16, 2025
9888a6a
feat: add conninfo helper
nakasyou Jul 17, 2025
1fd56e8
wip: test
nakasyou Jul 17, 2025
13820b3
test: add test
nakasyou Jul 17, 2025
d031e7a
fmt
nakasyou Jul 17, 2025
88cb51f
lint
nakasyou Jul 17, 2025
220f50c
feat: add to package.json
nakasyou Jul 17, 2025
72ed7c9
chore: remove jsr support
nakasyou Jul 17, 2025
39554f3
feat: enhance validateExports to support ignoring specified entries
nakasyou Jul 17, 2025
b3d2a86
fix: import order
nakasyou Jul 17, 2025
51a96ee
fix: make stack 512
nakasyou Jul 19, 2025
64f59cf
fmt
nakasyou Jul 20, 2025
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
6 changes: 5 additions & 1 deletion build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import path from 'path'
import { cleanupWorkers, removePrivateFields } from './remove-private-fields'
import { validateExports } from './validate-exports'

const PACKAGES_NOT_PUBLISHED_TO_JSR = [
'./moddable', // because the code depends on runtime-specific APIs can't be published to JSR
]

const args = arg({
'--watch': Boolean,
})
Expand All @@ -28,7 +32,7 @@ const readJsonExports = (path: string) => JSON.parse(fs.readFileSync(path, 'utf-
const [packageJsonExports, jsrJsonExports] = ['./package.json', './jsr.json'].map(readJsonExports)

// Validate exports of package.json and jsr.json
validateExports(packageJsonExports, jsrJsonExports, 'jsr.json')
validateExports(packageJsonExports, jsrJsonExports, 'jsr.json', PACKAGES_NOT_PUBLISHED_TO_JSR)
validateExports(jsrJsonExports, packageJsonExports, 'package.json')

const entryPoints = glob.sync('./src/**/*.ts', {
Expand Down
6 changes: 5 additions & 1 deletion build/validate-exports.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export const validateExports = (
source: Record<string, unknown>,
target: Record<string, unknown>,
fileName: string
fileName: string,
ignore: string[] = []
) => {
const isEntryInTarget = (entry: string): boolean => {
if (entry in target) {
Expand Down Expand Up @@ -31,6 +32,9 @@ export const validateExports = (

Object.keys(source).forEach((sourceEntry) => {
if (!isEntryInTarget(sourceEntry)) {
if (ignore.includes(sourceEntry)) {
return
}
throw new Error(`Missing "${sourceEntry}" in '${fileName}'`)
}
})
Expand Down
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"test:workerd": "vitest --run --config ./runtime-tests/workerd/vitest.config.ts",
"test:lambda": "vitest --run --config ./runtime-tests/lambda/vitest.config.ts",
"test:lambda-edge": "vitest --run --config ./runtime-tests/lambda-edge/vitest.config.ts",
"test:all": "bun run test && bun test:deno && bun test:bun && bun test:fastly && bun test:node && bun test:workerd && bun test:lambda && bun test:lambda-edge",
"test:moddable": "vitest --run --config ./runtime-tests/moddable/vitest.config.ts",
"test:all": "bun run test && bun test:deno && bun test:bun && bun test:fastly && bun test:node && bun test:workerd && bun test:lambda && bun test:lambda-edge && bun test:moddable",
"lint": "eslint src runtime-tests build perf-measures",
"lint:fix": "eslint src runtime-tests build perf-measures --fix",
"format": "prettier --check --cache \"src/**/*.{js,ts,tsx}\" \"runtime-tests/**/*.{js,ts,tsx}\" \"build/**/*.{js,ts,tsx}\" \"perf-measures/**/*.{js,ts,tsx}\"",
Expand Down Expand Up @@ -384,6 +385,11 @@
"import": "./dist/adapter/service-worker/index.js",
"require": "./dist/cjs/adapter/service-worker/index.js"
},
"./moddable": {
"types": "./dist/types/adapter/moddable/index.d.ts",
"import": "./dist/adapter/moddable/index.js",
"require": "./dist/cjs/adapter/moddable/index.js"
},
"./testing": {
"types": "./dist/types/helper/testing/index.d.ts",
"import": "./dist/helper/testing/index.js",
Expand Down Expand Up @@ -610,6 +616,9 @@
"service-worker": [
"./dist/types/adapter/service-worker"
],
"moddable": [
"./dist/types/adapter/moddable"
],
"testing": [
"./dist/types/helper/testing"
],
Expand Down
67 changes: 67 additions & 0 deletions runtime-tests/moddable/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { execSync, spawn } from 'node:child_process'

function isCommandAvailable(command: string): boolean {
try {
const checkCommand = process.platform === 'win32' ? `where ${command}` : `command -v ${command}`
execSync(checkCommand, { stdio: 'pipe' })
return true
} catch {
return false
}
}

const isModdableInstalled = isCommandAvailable('mcconfig')
let moddableEnvironment: 'win' | 'mac' | 'lin' | undefined = undefined
if (process.platform === 'win32') {
moddableEnvironment = 'win'
} else if (process.platform === 'darwin') {
moddableEnvironment = 'mac'
} else if (process.platform === 'linux') {
moddableEnvironment = 'lin'
} else {
throw new Error(`Unsupported platform: ${process.platform}`)
}
if (!moddableEnvironment) {
console.warn('Moddable environment not set, skipping tests.')
}

const skip = !isModdableInstalled || !moddableEnvironment

describe('moddable', { skip }, () => {
beforeAll(() => {
execSync(
'bun build runtime-tests/moddable/tests/main.ts --external socket --external streams --external text/decoder --external text/encoder --external headers --outdir runtime-tests/moddable/dist'
)
execSync('bunx kill-port 5002')
})
it(
'dist',
{
timeout: 1000 * 60 * 5, // 5 minutes
},
async () => {
const mcconfigProc = spawn('mcconfig', ['-m', '-dl', '-p', moddableEnvironment], {
cwd: 'runtime-tests/moddable',
})
await new Promise<void>((resolve, reject) => {
mcconfigProc.on('error', (err) => {
reject(err)
})
let output = ''
mcconfigProc.stdout.on('data', (data) => {
output += data.toString()
if (output.includes('connected to "moddable"')) {
resolve()
}
})
})
expect(await fetch('http://localhost:3000').then((res) => res.text())).toEqual(
'{"hono":"moddable"}'
)
mcconfigProc.kill('SIGSTOP')
}
)
afterAll(() => {
console.log('Stopping Moddable environment...')
})
})
22 changes: 22 additions & 0 deletions runtime-tests/moddable/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"include": [
"$(MODDABLE)/examples/manifest_base.json",
"$(MODDABLE)/examples/manifest_net.json",
"$(MODULES)/network/http/manifest.json",
"$(MODDABLE)/modules/data/headers/manifest.json",
"$(MODDABLE)/modules/data/url/manifest.json",
"$(MODDABLE)/modules/data/text/encoder/manifest.json",
"$(MODDABLE)/modules/data/text/decoder/manifest.json",
"$(MODDABLE)/modules/data/headers/manifest.json",
"$(MODDABLE)/modules/network/websocket/manifest.json"
],
"modules": {
"*": [
"$(MODDABLE)/examples/io/streams/modules/streams.js",
"./dist/main.js"
]
},
"creation": {
"stack": 512
}
}
14 changes: 14 additions & 0 deletions runtime-tests/moddable/tests/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// @ts-expect-error Internal API
import { Listener } from 'socket'
import { Hono } from '../../../src'
import { handle } from '../../../src/adapter/moddable'
import { cors } from '../../../src/middleware/cors'

const app = new Hono().use(cors()).get('/', (c) =>
c.json({
hono: 'moddable',
})
)

const listener = new Listener({ port: 3000 })
listener.callback = handle(app)
19 changes: 19 additions & 0 deletions runtime-tests/moddable/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
import config from '../../vitest.config'

export default defineConfig({
test: {
globals: true,
include: ['**/runtime-tests/moddable/**/*.+(ts|tsx|js)'],
exclude: [
'**/runtime-tests/moddable/vitest.config.ts',
'**/runtime-tests/moddable/tests/**',
'**/runtime-tests/moddable/dist/**',
],
coverage: {
...config.test?.coverage,
reportsDirectory: './coverage/raw/moddable',
},
},
})
38 changes: 38 additions & 0 deletions src/adapter/moddable/conninfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Context } from '../../context'
import { getConnInfo } from './conninfo'

describe('getConnInfo', () => {
it('Should throw an error if socket is not available', () => {
const c: Context = {
env: {},
} as unknown as Context
expect(() => getConnInfo(c)).toThrow(TypeError)
})
it('Should return empty remote address if REMOTE_IP is not available', () => {
const c: Context = {
env: {
socket: {
get: () => undefined,
},
},
} as unknown as Context
expect(getConnInfo(c)).toEqual({
remote: {},
})
})
it('Should return remote address and transport type', () => {
const c: Context = {
env: {
socket: {
get: () => '1.1.1.1',
},
},
} as unknown as Context
expect(getConnInfo(c)).toEqual({
remote: {
address: '1.1.1.1',
transport: 'tcp',
},
})
})
})
32 changes: 32 additions & 0 deletions src/adapter/moddable/conninfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Context } from '../..'
import type { GetConnInfo } from '../../helper/conninfo'

/**
* Get ConnInfo with Moddable
* @param c Context
* @returns ConnInfo
*/
export const getConnInfo: GetConnInfo = (c: Context) => {
const socket = c.env.socket as
| {
get: (type: 'REMOTE_IP') => string | undefined
}
| undefined

if (!socket) {
throw new TypeError('env has to include the socket object.')
}

const addr = socket.get('REMOTE_IP')
if (!addr) {
return {
remote: {},
}
}
return {
remote: {
address: addr,
transport: 'tcp',
},
}
}
90 changes: 90 additions & 0 deletions src/adapter/moddable/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Socket, SocketConstructor } from './handler'
import { createHandleFunction } from './handler'

const encoder = new TextEncoder()
const decoder = new TextDecoder()

describe('handler', () => {
it('Should create a HTTP response', async () => {
let MockSocket!: SocketConstructor
let socket!: Socket
const responsePromise = new Promise((resolve) => {
let response: string = ''
MockSocket = class implements Socket {
constructor() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
socket = this
}
callback: (
this: { read(type: typeof ArrayBuffer): ArrayBuffer },
message: number,
value?: unknown
) => void = () => null
write(chunk: ArrayBuffer): void {
response += decoder.decode(chunk)
}
close(): void {
resolve(response)
}
}
})
const handle = createHandleFunction(MockSocket)
const callback = handle({
fetch: () => new Response('Hello World'),
})
callback.call({
callback: () => {},
})
socket.callback.call(
{
read() {
return encoder.encode('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n').buffer
},
},
2
)
expect(await responsePromise).toEqual(
'HTTP/1.1 200 \r\ncontent-type: text/plain;charset=UTF-8\r\n\r\nHello World'
)
})
it('Should close the socket on invalid request', async () => {
let MockSocket!: SocketConstructor
let socket!: Socket
const responsePromise = new Promise((resolve) => {
let response: string = ''
MockSocket = class implements Socket {
constructor() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
socket = this
}
callback: (
this: { read(type: typeof ArrayBuffer): ArrayBuffer },
message: number,
value?: unknown
) => void = () => null
write(chunk: ArrayBuffer): void {
response += decoder.decode(chunk)
}
close(): void {
resolve(response)
}
}
})
const handle = createHandleFunction(MockSocket)
const callback = handle({
fetch: () => new Response('Hello World'),
})
callback.call({
callback: () => {},
})
socket.callback.call(
{
read() {
return encoder.encode('aa\r\n\r\n').buffer
},
},
2
)
expect(await responsePromise).toEqual('')
})
})
Loading
Loading