Skip to content
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
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"build:watch": "tsc --watch",
"typecheck": "tsc --noEmit",
"lint": "tsc --noEmit",
"test": "tsx test/translate.test.ts",
"test:unit": "tsx test/translate.test.ts",
"test": "npx tsx --test test/*.test.ts",
"test:unit": "npx tsx --test test/*.test.ts",
"test:bats": "npm run build && bats test/cli.bats",
"test:e2e": "npm run build && bats test/cli.bats test/e2e.bats"
},
Expand Down
83 changes: 60 additions & 23 deletions packages/cli/src/bundle-sdk/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs, { promises as fsp } from 'node:fs'
import path from 'node:path'
import type { Logger } from '../logger.js'
import type { Logger } from '@/logger'

interface BarePackHeader {
id?: string
Expand Down Expand Up @@ -137,6 +137,47 @@ export function extractBarePackHeader (packed: string): BarePackHeader {
return JSON.parse(packed.slice(jsonStart, i)) as BarePackHeader
}

// Global flag: capture all node_modules segments including nested ones
const NODE_MODULES_RE = /\/node_modules\/(@[^/]+\/[^/]+|[^/]+)(?=\/)/g

export function extractPackageNamesFromResolutions (resolutions: Record<string, unknown>): Set<string> {
const names = new Set<string>()
for (const key of Object.keys(resolutions)) {
for (const match of key.matchAll(NODE_MODULES_RE)) {
if (match[1]) names.add(match[1])
}
}
return names
}

function buildNestedPathIndex (
resolutions: Record<string, unknown>,
projectRoot: string
): Map<string, Set<string>> {
const index = new Map<string, Set<string>>()
for (const key of Object.keys(resolutions)) {
for (const match of key.matchAll(NODE_MODULES_RE)) {
const pkgName = match[1]
if (!pkgName) continue
const marker = `/node_modules/${pkgName}/`
const idx = key.indexOf(marker)
if (idx === -1) continue
const candidate = path.join(
projectRoot,
key.slice(1, idx + marker.length),
'package.json'
)
let set = index.get(pkgName)
if (!set) {
set = new Set()
index.set(pkgName, set)
}
set.add(candidate)
}
}
return index
}

export async function generateAddonsManifest (options: GenerateAddonsManifestOptions): Promise<GenerateAddonsManifestResult> {
const { bundlePath, outputDir, projectRoot, logger } = options

Expand All @@ -147,33 +188,29 @@ export async function generateAddonsManifest (options: GenerateAddonsManifestOpt
const header = extractBarePackHeader(packed)
const resolutions = header.resolutions ?? {}

const packageNames = new Set<string>()
const nodeModulesRegex = /\/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//

for (const key of Object.keys(resolutions)) {
const match = key.match(nodeModulesRegex)
if (match?.[1]) {
packageNames.add(match[1])
}
}
const packageNames = extractPackageNamesFromResolutions(resolutions)
const nestedPaths = buildNestedPathIndex(resolutions, projectRoot)

const addons: string[] = []
for (const pkgName of packageNames) {
const pkgJsonPath = path.join(
projectRoot,
'node_modules',
pkgName,
'package.json'
)
try {
if (fs.existsSync(pkgJsonPath)) {
const pkgJson = JSON.parse(await fsp.readFile(pkgJsonPath, 'utf8')) as { addon?: boolean }
if (pkgJson.addon === true) {
addons.push(pkgName)
const candidates = [
path.join(projectRoot, 'node_modules', pkgName, 'package.json'),
...(nestedPaths.get(pkgName) ?? [])
]

let pkgJson: { addon?: boolean } | null = null
for (const candidate of candidates) {
try {
if (fs.existsSync(candidate)) {
pkgJson = JSON.parse(await fsp.readFile(candidate, 'utf8')) as { addon?: boolean }
break
}
} catch (err) {
logger.warn(` Could not read ${candidate}: ${(err as Error).message}`)
}
} catch (err) {
logger.warn(` Could not read ${pkgName}/package.json: ${(err as Error).message}`)
}
if (pkgJson?.addon === true) {
addons.push(pkgName)
}
}

Expand Down
135 changes: 135 additions & 0 deletions packages/cli/test/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
extractPackedString,
extractBarePackHeader,
extractPackageNamesFromResolutions
} from '../src/bundle-sdk/manifest.js'

describe('extractPackageNamesFromResolutions', () => {
it('captures both parent and nested packages', () => {
const names = extractPackageNamesFromResolutions({
'/node_modules/@qvac/sdk/node_modules/bare-abort/binding.js': {}
})
assert.ok(names.has('@qvac/sdk'))
assert.ok(names.has('bare-abort'))
assert.equal(names.size, 2)
})

it('captures scoped nested packages', () => {
const names = extractPackageNamesFromResolutions({
'/node_modules/@qvac/sdk/node_modules/@qvac/llm-llamacpp/index.js': {}
})
assert.deepEqual([...names].sort(), ['@qvac/llm-llamacpp', '@qvac/sdk'])
})

it('captures single-level packages', () => {
const names = extractPackageNamesFromResolutions({
'/node_modules/mqtt/dist/mqtt.js': {}
})
assert.deepEqual([...names], ['mqtt'])
})

it('captures three-level nesting', () => {
const names = extractPackageNamesFromResolutions({
'/node_modules/@qvac/sdk/node_modules/bare-fs/node_modules/bare-stream/index.js': {}
})
assert.deepEqual([...names].sort(), ['@qvac/sdk', 'bare-fs', 'bare-stream'])
})

it('ignores paths without node_modules', () => {
const names = extractPackageNamesFromResolutions({
'/src/utils/helper.js': {}
})
assert.equal(names.size, 0)
})

it('deduplicates across multiple resolution keys', () => {
const names = extractPackageNamesFromResolutions({
'/node_modules/@qvac/sdk/node_modules/bare-abort/binding.js': {},
'/node_modules/@qvac/sdk/node_modules/bare-abort/index.js': {},
'/node_modules/@qvac/sdk/node_modules/bare-os/binding.js': {},
'/node_modules/@qvac/sdk/dist/constants/audio.js': {},
'/node_modules/mqtt/dist/mqtt.js': {}
})
assert.deepEqual(
[...names].sort(),
['@qvac/sdk', 'bare-abort', 'bare-os', 'mqtt']
)
})

it('old regex (without /g) would miss nested packages', () => {
const oldRegex = /\/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//
const key = '/node_modules/@qvac/sdk/node_modules/bare-abort/binding.js'

const oldMatch = key.match(oldRegex)
assert.equal(oldMatch?.[1], '@qvac/sdk')

const names = extractPackageNamesFromResolutions({ [key]: {} })
assert.ok(names.has('bare-abort'), 'new implementation must capture nested package')
})
})

describe('extractPackedString', () => {
it('extracts double-quoted string', () => {
assert.equal(
extractPackedString('module.exports = "hello world"'),
'hello world'
)
})

it('extracts single-quoted string', () => {
assert.equal(
extractPackedString("module.exports = 'hello world'"),
'hello world'
)
})

it('handles escape sequences', () => {
assert.equal(
extractPackedString('module.exports = "line1\\nline2\\ttab"'),
'line1\nline2\ttab'
)
})

it('throws on missing module.exports', () => {
assert.throws(() => extractPackedString('const x = 1'), /module\.exports/)
})

it('throws on non-string export', () => {
assert.throws(() => extractPackedString('module.exports = 42'), /not a string/)
})
})

describe('extractBarePackHeader', () => {
it('extracts JSON header from packed string', () => {
const header = extractBarePackHeader(
'some-id\n{"id":"abc","resolutions":{"key":"val"}}\nrest'
)
assert.equal(header.id, 'abc')
assert.deepEqual(header.resolutions, { key: 'val' })
})

it('handles nested JSON in header', () => {
const header = extractBarePackHeader(
'id\n{"id":"x","resolutions":{"/node_modules/foo/index.js":{"a":1}}}\ndata'
)
assert.equal(header.id, 'x')
assert.ok(header.resolutions)
assert.ok('/node_modules/foo/index.js' in (header.resolutions as Record<string, unknown>))
})

it('throws on missing first newline', () => {
assert.throws(
() => extractBarePackHeader('no newline here'),
/missing first newline/
)
})

it('throws on missing JSON', () => {
assert.throws(
() => extractBarePackHeader('id\nno json here'),
/could not find header JSON/
)
})
})
Loading