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
1 change: 0 additions & 1 deletion apps/blog-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@holo-js/core": "workspace:*",
"@holo-js/db": "workspace:*",
"@holo-js/db-sqlite": "workspace:*",
"@holo-js/events": "workspace:*",
"@holo-js/flux": "workspace:*",
"@holo-js/flux-react": "workspace:*",
"@holo-js/forms": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion apps/blog-nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@holo-js/core": "workspace:*",
"@holo-js/db": "workspace:*",
"@holo-js/db-sqlite": "workspace:*",
"@holo-js/events": "workspace:*",
"@holo-js/flux": "workspace:*",
"@holo-js/flux-vue": "workspace:*",
"@holo-js/forms": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion apps/blog-sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@holo-js/core": "workspace:*",
"@holo-js/db": "workspace:*",
"@holo-js/db-sqlite": "workspace:*",
"@holo-js/events": "workspace:*",
"@holo-js/flux": "workspace:*",
"@holo-js/flux-svelte": "workspace:*",
"@holo-js/forms": "workspace:*",
Expand Down
1 change: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions packages/adapter-nuxt/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { access, mkdir, readdir, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { basename, dirname, extname, relative, resolve } from 'node:path'
import {
addImports,
Expand Down Expand Up @@ -77,6 +78,17 @@ type StorageS3Module = {
}

const MODEL_FILE_EXTENSIONS = new Set(['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'])
const HOLO_PACKAGE_SCOPE = '@holo-js/'
const CLIENT_OPTIMIZE_DEPS = [
{
packageName: `${HOLO_PACKAGE_SCOPE}forms`,
include: `${HOLO_PACKAGE_SCOPE}forms > ${HOLO_PACKAGE_SCOPE}validation > valibot`,
},
{
packageName: `${HOLO_PACKAGE_SCOPE}validation`,
include: `${HOLO_PACKAGE_SCOPE}validation > valibot`,
},
] as const

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
Expand Down Expand Up @@ -109,13 +121,35 @@ function hasModuleNotFoundCode(error: unknown, expectedSpecifier: string): boole
return false
}

function isModuleResolutionFailure(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false
}

return 'code' in error
&& (
(error as { code?: unknown }).code === 'MODULE_NOT_FOUND'
|| (error as { code?: unknown }).code === 'ERR_MODULE_NOT_FOUND'
)
}

interface NuxtHookContext {
hook: (
name: string,
callback: (payload: { references: Array<{ path?: string, types?: string }> }) => void,
) => void
}

type ViteOptimizeDepsOptions = {
include?: string[]
[key: string]: unknown
}

type NuxtViteOptions = {
optimizeDeps?: ViteOptimizeDepsOptions
[key: string]: unknown
}

interface NuxtOptionsWithNitro {
nitro: {
storage: Record<string, unknown>
Expand All @@ -137,6 +171,7 @@ interface NuxtOptionsWithNitro {
[key: string]: unknown
}
build: { transpile: string[] }
vite?: NuxtViteOptions
srcDir: string
rootDir?: string
_holoStorageModuleOptions?: StorageModuleOptions
Expand All @@ -146,6 +181,40 @@ interface NuxtOptionsWithNitro {
_holoTypesRegistered?: boolean
}

function resolveClientOptimizeDeps(rootDir: string): string[] {
const projectRequire = createRequire(resolve(rootDir, 'package.json'))
const deps: string[] = []

for (const { packageName, include } of CLIENT_OPTIMIZE_DEPS) {
try {
projectRequire.resolve(packageName)
deps.push(include)
} catch (error) {
/* v8 ignore next 3 -- unexpected resolver failures should fail module setup */
if (!isModuleResolutionFailure(error)) {
throw error
}
}
}

return deps
}

function addViteOptimizeDeps(opts: NuxtOptionsWithNitro, deps: readonly string[]): void {
if (deps.length === 0) {
return
}

opts.vite = opts.vite || {}
opts.vite.optimizeDeps = opts.vite.optimizeDeps || {}
opts.vite.optimizeDeps.include = [
...new Set([
...(opts.vite.optimizeDeps.include || []),
...deps,
]),
]
}

/* v8 ignore next 15 -- optional-package absence is validated in published-package integration, not in this monorepo test graph */
async function importOptionalStorageModule(): Promise<StorageModule | undefined> {
try {
Expand Down Expand Up @@ -278,6 +347,7 @@ export default defineNuxtModule<ModuleOptions>({
const rootDir = opts.rootDir ?? opts.srcDir ?? process.cwd()
const sourceDir = opts.srcDir ?? rootDir
const modelRegistryTypesPath = resolve(rootDir, '.holo-js/generated/model-registry.d.ts')
addViteOptimizeDeps(opts, resolveClientOptimizeDeps(rootDir))
const loaded = await loadConfigDirectory(rootDir, {
preferCache: process.env.NODE_ENV === 'production',
processEnv: process.env,
Expand Down Expand Up @@ -399,9 +469,12 @@ export default defineNuxtModule<ModuleOptions>({
})

export const moduleInternals = {
addViteOptimizeDeps,
hasModuleNotFoundCode,
hasLoadedConfigFile,
importOptionalStorageS3Module,
isModuleResolutionFailure,
resolveClientOptimizeDeps,
}

export const adapterNuxtInternals = {
Expand Down
76 changes: 76 additions & 0 deletions packages/adapter-nuxt/tests/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ type NuxtHarnessOptions = {
build: {
transpile: string[]
}
vite?: {
optimizeDeps?: {
include?: string[]
[key: string]: unknown
}
[key: string]: unknown
}
nitro?: {
storage?: Record<string, unknown>
}
Expand Down Expand Up @@ -172,13 +179,23 @@ function getHoloStorageRuntimeConfig(nuxt: NuxtHarness): Record<string, unknown>
return nuxt.options.runtimeConfig?.holoStorage as Record<string, unknown> | undefined
}

function getOptimizeDepsInclude(nuxt: NuxtHarness): readonly string[] {
return nuxt.options.vite?.optimizeDeps?.include ?? []
}

async function createProject(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'holo-storage-module-'))
tempDirs.push(root)
await mkdir(join(root, 'config'), { recursive: true })
return root
}

async function linkHoloPackage(root: string, name: string): Promise<void> {
const scopedPackageDir = join(root, 'node_modules/@holo-js')
await mkdir(scopedPackageDir, { recursive: true })
await symlink(resolve(packageDir, `../${name}`), join(scopedPackageDir, name))
}

async function loadAdapterModule() {
vi.resetModules()

Expand All @@ -202,6 +219,7 @@ async function loadAdapterModule() {

return {
module: mod.default,
moduleInternals: mod.moduleInternals,
addImports,
addServerImportsDir,
addServerHandler,
Expand Down Expand Up @@ -358,6 +376,64 @@ describe('@holo-js/adapter-nuxt module setup', () => {
expect(publishedPlugin).toContain('from "nitropack/runtime/config"')
}, 60000)

it('resolves installed Holo client optimizer dependencies from the project root', async () => {
const root = await createProject()
const { moduleInternals } = await loadAdapterModule()

expect(moduleInternals.resolveClientOptimizeDeps(root)).toEqual([])
expect(moduleInternals.isModuleResolutionFailure(null)).toBe(false)
expect(moduleInternals.isModuleResolutionFailure({ code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' })).toBe(false)
expect(moduleInternals.isModuleResolutionFailure({ code: 'MODULE_NOT_FOUND' })).toBe(true)
expect(moduleInternals.isModuleResolutionFailure({ code: 'ERR_MODULE_NOT_FOUND' })).toBe(true)

const nuxt = createNuxtHarness(root)
moduleInternals.addViteOptimizeDeps(nuxt.options as never, [])
expect(nuxt.options.vite).toBeUndefined()

moduleInternals.addViteOptimizeDeps(nuxt.options as never, ['@holo-js/validation > valibot'])
expect(getOptimizeDepsInclude(nuxt)).toEqual(['@holo-js/validation > valibot'])

await linkHoloPackage(root, 'validation')
expect(moduleInternals.resolveClientOptimizeDeps(root)).toEqual([
'@holo-js/validation > valibot',
])

await linkHoloPackage(root, 'forms')
expect(moduleInternals.resolveClientOptimizeDeps(root)).toEqual([
'@holo-js/forms > @holo-js/validation > valibot',
'@holo-js/validation > valibot',
])
})

it('merges Holo client optimizer dependencies without replacing user Vite options', async () => {
const root = await createProject()
await mkdir(join(root, '.holo-js/generated'), { recursive: true })
await writeFile(join(root, '.holo-js/generated/model-registry.d.ts'), 'export {}\n', 'utf8')
await linkHoloPackage(root, 'forms')
await linkHoloPackage(root, 'validation')

const { module } = await loadAdapterModule()
const nuxt = createNuxtHarness(root)
nuxt.options.vite = {
optimizeDeps: {
include: [
'vue',
'@holo-js/validation > valibot',
],
exclude: ['vue-demi'],
},
}

await module.setup({}, nuxt as never)

expect(getOptimizeDepsInclude(nuxt)).toEqual([
'vue',
'@holo-js/validation > valibot',
'@holo-js/forms > @holo-js/validation > valibot',
])
expect(nuxt.options.vite.optimizeDeps?.exclude).toEqual(['vue-demi'])
}, 30000)

it('loads storage config files from the project root and wires nitro/runtime state', async () => {
const root = await createProject()
await writeFile(join(root, '.env'), 'STORAGE_DEFAULT_DISK=media\n', 'utf8')
Expand Down