Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
934 changes: 740 additions & 194 deletions bun.lock

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
"apps/*",
"playground"
],
"catalog": {
"catalog": {
"better-sqlite3": "^11.7.0",
"@types/better-sqlite3": "^7.6.12",
"nuxt": "^4.0.0",
"vue": "^3.5.13",
"vue-router": "^5.0.4",
"vite": "^5.4.14",
"@nuxt/kit": "^4.0.0",
"@nuxt/module-builder": "^1.0.2",
"@eslint/js": "^9.17.0",
Expand Down Expand Up @@ -58,6 +61,10 @@
"test:cli": "vitest --run --project @holo-js/cli",
"test:cli:integration": "bun run --cwd packages/cli test:integration",
"test:cli:coverage": "rm -rf coverage/cli && HOLO_CLI_INCLUDE_INTEGRATION=1 vitest --run --project @holo-js/cli --coverage --coverage.clean=false --coverage.reportsDirectory='coverage/cli' --coverage.include='packages/cli/src/**/*.ts' --coverage.exclude='packages/cli/src/**/types.ts' --coverage.exclude='packages/cli/src/cli-types.ts' --coverage.exclude='packages/cli/src/bin/**' --pool forks",
"test:example:blog-nuxt": "bun run --cwd apps/blog-nuxt test",
"test:example:blog-next": "bun run --cwd apps/blog-next test",
"test:example:blog-sveltekit": "bun run --cwd apps/blog-sveltekit test",
"test:examples": "bun run test:example:blog-nuxt && bun run test:example:blog-next && bun run test:example:blog-sveltekit",
"test:config:coverage": "rm -rf coverage/config && vitest --run --project @holo-js/config --coverage --coverage.clean=false --coverage.reportsDirectory='coverage/config' --coverage.include='packages/config/src/**/*.ts' --coverage.exclude='packages/config/src/**/types.ts'",
"test:core:coverage": "rm -rf coverage/core && vitest --run --project @holo-js/core --coverage --coverage.clean=false --coverage.reportsDirectory='coverage/core' --coverage.include='packages/core/src/**/*.ts' --coverage.exclude='packages/core/src/**/types.ts' --coverage.exclude='packages/core/src/runtime/**/*.d.ts'",
"test:db:coverage": "rm -rf coverage/db && vitest --run --project @holo-js/db --coverage --coverage.clean=false --coverage.reportsDirectory='coverage/db' --coverage.include='packages/db/src/**/*.ts' --coverage.exclude='packages/db/src/**/types.ts' --coverage.exclude='packages/db/src/migrations/templates/**' --coverage.exclude='packages/db/src/drivers/index.ts' --coverage.exclude='packages/db/src/drivers/SQLiteAdapter.ts' --coverage.exclude='packages/db/src/drivers/PostgresAdapter.ts' --coverage.exclude='packages/db/src/drivers/MySQLAdapter.ts'",
Expand Down
12 changes: 12 additions & 0 deletions packages/adapter-nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ interface NuxtOptionsWithNitro {
[key: string]: unknown
}
runtimeConfig: {
public?: {
holo?: {
appName?: string
[key: string]: unknown
}
[key: string]: unknown
}
holoStorage?: HoloStorageRuntimeConfig
[key: string]: unknown
}
Expand Down Expand Up @@ -241,6 +248,11 @@ export default defineNuxtModule<ModuleOptions>({
opts.nitro = opts.nitro || { storage: {} }
opts.nitro.storage = opts.nitro.storage || {}
opts.runtimeConfig = opts.runtimeConfig || {}
opts.runtimeConfig.public = opts.runtimeConfig.public || {}
opts.runtimeConfig.public.holo = {
...(opts.runtimeConfig.public.holo || {}),
appName: loaded.app.name,
}
opts.runtimeConfig.holo = {
appUrl: loaded.app.url,
appEnv: loaded.app.env,
Expand Down
5 changes: 5 additions & 0 deletions packages/adapter-nuxt/tests/module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ export default defineDatabaseConfig({
appEnv: 'production',
projectRoot: root,
})
expect((nuxt.options.runtimeConfig.public as Record<string, unknown>)).toMatchObject({
holo: {
appName: 'Example App',
},
})
expect(nuxt.options.runtimeConfig.db).toEqual({
defaultConnection: 'primary',
connections: {
Expand Down
5 changes: 5 additions & 0 deletions packages/adapter-sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"types": "./dist/client.d.ts",
"import": "./dist/client.mjs",
"default": "./dist/client.mjs"
},
"./transport": {
"types": "./dist/transport.d.ts",
"import": "./dist/transport.mjs",
"default": "./dist/transport.mjs"
}
},
"main": "./dist/index.mjs",
Expand Down
4 changes: 4 additions & 0 deletions packages/adapter-sveltekit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
type HoloFrameworkOptions,
} from '@holo-js/core'
import type { HoloConfigMap } from '@holo-js/config'
export {
holoSvelteKitTransport,
type SvelteKitTransportDefinition,
} from './transport'

export type SvelteKitHoloOptions = HoloFrameworkOptions

Expand Down
95 changes: 95 additions & 0 deletions packages/adapter-sveltekit/src/transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
type JsonSerializable = {
toJSON(): unknown
}

type HoloModelEntityLike = JsonSerializable & {
getRepository(): unknown
toAttributes(): Record<string, unknown>
}

type HoloModelCollectionLike = JsonSerializable & {
modelKeys(): unknown[]
toQuery(): unknown
}

export type SerializedSvelteKitData<TValue>
= TValue extends JsonSerializable
? ReturnType<TValue['toJSON']>
: TValue extends readonly (infer TItem)[]
? SerializedSvelteKitData<TItem>[]
: TValue extends Date
? Date
: TValue extends object
? { [K in keyof TValue]: SerializedSvelteKitData<TValue[K]> }
: TValue

type HoloTransportEncoder = {
encode: (value: unknown) => false | unknown[] | Record<string, unknown>
decode: (value: unknown[] | Record<string, unknown>) => unknown
}

export type SvelteKitTransportDefinition = Readonly<{
HoloModel: HoloTransportEncoder
HoloCollection: HoloTransportEncoder
}>

function isHoloModelEntityLike(value: unknown): value is HoloModelEntityLike {
return Boolean(
value
&& typeof value === 'object'
&& typeof (value as HoloModelEntityLike).getRepository === 'function'
&& typeof (value as HoloModelEntityLike).toAttributes === 'function'
&& typeof (value as HoloModelEntityLike).toJSON === 'function',
)
}

function isHoloModelCollectionLike(value: unknown): value is HoloModelCollectionLike {
const candidate = value as unknown as HoloModelCollectionLike
return Boolean(
Array.isArray(value)
&& typeof candidate.modelKeys === 'function'
&& typeof candidate.toQuery === 'function'
&& typeof candidate.toJSON === 'function',
)
}

export function serializeSvelteKitData<TValue>(value: TValue): SerializedSvelteKitData<TValue> {
if (isHoloModelEntityLike(value) || isHoloModelCollectionLike(value)) {
return value.toJSON() as SerializedSvelteKitData<TValue>
}

if (Array.isArray(value)) {
return value.map(item => serializeSvelteKitData(item)) as SerializedSvelteKitData<TValue>
}

if (value instanceof Date || value === null || typeof value !== 'object') {
return value as SerializedSvelteKitData<TValue>
}

return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, serializeSvelteKitData(entry)]),
) as SerializedSvelteKitData<TValue>
}

export const holoSvelteKitTransport: SvelteKitTransportDefinition = {
HoloModel: {
encode(value) {
return isHoloModelEntityLike(value)
? value.toJSON() as Record<string, unknown>
: false
},
decode(value) {
return value
},
},
HoloCollection: {
encode(value) {
return isHoloModelCollectionLike(value)
? value.toJSON() as unknown[]
: false
},
decode(value) {
return value
},
},
}
51 changes: 51 additions & 0 deletions packages/adapter-sveltekit/tests/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,57 @@ afterAll(async () => {
})

describe('@holo-js/adapter-sveltekit', () => {
it('serializes model-like values for SvelteKit transport boundaries', async () => {
const { holoSvelteKitTransport } = await loadAdapterModule()
const { serializeSvelteKitData } = await import('../src/transport')

const entity = {
id: 1,
title: 'Hello',
getRepository() {
return {}
},
toAttributes() {
return { id: this.id, title: this.title }
},
toJSON() {
return { id: this.id, title: this.title, kind: 'entity' }
},
}
const collection = Object.assign([entity], {
modelKeys() {
return [entity.id]
},
toQuery() {
return {}
},
toJSON() {
return [{ id: entity.id, title: entity.title, kind: 'entity' }]
},
})

expect(serializeSvelteKitData({
entity,
collection,
nested: { entity },
})).toEqual({
entity: { id: 1, title: 'Hello', kind: 'entity' },
collection: [{ id: 1, title: 'Hello', kind: 'entity' }],
nested: {
entity: { id: 1, title: 'Hello', kind: 'entity' },
},
})

expect(holoSvelteKitTransport.HoloModel.encode(entity)).toEqual({
id: 1,
title: 'Hello',
kind: 'entity',
})
expect(holoSvelteKitTransport.HoloCollection.encode(collection)).toEqual([
{ id: 1, title: 'Hello', kind: 'entity' },
])
}, 30000)

it('initializes a singleton project and exposes typed config helpers', async () => {
const {
adapterSvelteKitInternals,
Expand Down
39 changes: 39 additions & 0 deletions packages/adapter-sveltekit/tests/adapter.type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { createSvelteKitHoloHelpers } from '../src'
import type { SerializedSvelteKitData } from '../src/transport'

const packageDir = resolve(import.meta.dirname, '..')

Expand Down Expand Up @@ -61,6 +62,44 @@ describe('@holo-js/adapter-sveltekit typing', () => {
void secret
})

it('preserves serialized payload inference for transport serialization', async () => {
const model = {
id: 1,
name: 'Amina',
toJSON() {
return {
id: this.id,
name: this.name,
}
},
}

type LoadResult = SerializedSvelteKitData<{
user: typeof model
users: typeof model[]
}>
type SerializedValue = SerializedSvelteKitData<typeof model>
type UserResult = LoadResult extends { user: infer TResult } ? TResult : never
type UsersResult = LoadResult extends { users: readonly (infer TResult)[] } ? TResult : never

const userResult: UserResult = {
id: 1,
name: 'Amina',
}
const usersResult: UsersResult = {
id: 1,
name: 'Amina',
}
const serializedValue: SerializedValue = {
id: 1,
name: 'Amina',
}

void userResult
void usersResult
void serializedValue
})

it('publishes a client declaration that type-checks under NodeNext resolution', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'holo-sveltekit-client-types-'))
const tempNodeModules = join(tempDir, 'node_modules')
Expand Down
1 change: 1 addition & 0 deletions packages/adapter-sveltekit/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default defineConfig({
entry: {
index: 'src/index.ts',
client: 'src/client.ts',
transport: 'src/transport.ts',
},
format: ['esm'],
dts: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/tests/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ describe('@holo-js/auth package runtime', () => {
readonly peerDependenciesMeta?: Record<string, { readonly optional?: boolean }>
}

expect(packageJson.peerDependencies?.['@holo-js/security']).toBe('^0.1.2')
expect(packageJson.peerDependencies?.['@holo-js/security']).toBe('^0.1.4')
expect(packageJson.peerDependenciesMeta?.['@holo-js/security']?.optional).toBe(true)
})

Expand Down
4 changes: 2 additions & 2 deletions packages/cache-db/tests/package.type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ describe('@holo-js/cache-db typing', () => {
if (false) {
const query = User.query()

expectTypeOf(User.with('team')).toMatchTypeOf<UserQuery>()
expectTypeOf(query.with('team')).toMatchTypeOf<UserQuery>()
void User.with('team')
void query.with('team')
expectTypeOf(User.definition.relations).toMatchTypeOf<UserRelations>()

// @ts-expect-error invalid generated-schema scope names should be rejected
Expand Down
Loading