mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: add host and port support to TuonoConfig (#366)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
import type { InternalTuonoConfig } from '../types'
|
||||
|
||||
import {
|
||||
DOT_TUONO_FOLDER_NAME,
|
||||
CONFIG_FOLDER_NAME,
|
||||
SERVER_CONFIG_NAME,
|
||||
} from '../constants'
|
||||
|
||||
const CONFIG_PATH = path.join(
|
||||
DOT_TUONO_FOLDER_NAME,
|
||||
CONFIG_FOLDER_NAME,
|
||||
SERVER_CONFIG_NAME,
|
||||
)
|
||||
/**
|
||||
* This function is used to remove the `vite` property from the config object.
|
||||
* The `vite` property is only used at build time, so it is not needed by either the server or the client.
|
||||
*/
|
||||
function removeViteProperties(
|
||||
config: InternalTuonoConfig,
|
||||
): Omit<InternalTuonoConfig, 'vite'> {
|
||||
const newConfig = structuredClone(config)
|
||||
delete newConfig['vite']
|
||||
return newConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* This function creates a JSON config file for the server,
|
||||
* that will be shared with the client as a prop.
|
||||
* The file will be saved at`.tuono/config/config.json`.
|
||||
*
|
||||
* The file is in JSON format to ensure it's easily read by the server,
|
||||
* which is written in Rust.
|
||||
*/
|
||||
export async function createJsonConfig(
|
||||
config: InternalTuonoConfig,
|
||||
): Promise<void> {
|
||||
const jsonConfig = removeViteProperties(config)
|
||||
|
||||
const fullPath = path.resolve(CONFIG_PATH)
|
||||
const jsonContent = JSON.stringify(jsonConfig)
|
||||
|
||||
// No need to manage error state. Tuono CLI will manage it.
|
||||
await fs.writeFile(fullPath, jsonContent, 'utf-8')
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { loadConfig } from './load-config'
|
||||
export { createJsonConfig } from './create-json-config'
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { loadConfig } from './load-config'
|
||||
|
||||
describe('loadConfig', () => {
|
||||
it('should error if the config does not exist', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
await loadConfig()
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
import type { TuonoConfig } from '../../config'
|
||||
|
||||
import type { InternalTuonoConfig } from '../types'
|
||||
|
||||
import {
|
||||
DOT_TUONO_FOLDER_NAME,
|
||||
CONFIG_FOLDER_NAME,
|
||||
CONFIG_FILE_NAME,
|
||||
} from '../constants'
|
||||
|
||||
import { normalizeConfig } from './normalize-config'
|
||||
|
||||
export const loadConfig = async (): Promise<InternalTuonoConfig> => {
|
||||
try {
|
||||
const configFile = (await import(
|
||||
pathToFileURL(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
DOT_TUONO_FOLDER_NAME,
|
||||
CONFIG_FOLDER_NAME,
|
||||
CONFIG_FILE_NAME,
|
||||
),
|
||||
).href
|
||||
)) as { default: TuonoConfig }
|
||||
|
||||
return normalizeConfig(configFile.default)
|
||||
} catch (err) {
|
||||
console.error('Failed to load tuono.config.ts')
|
||||
console.error(err)
|
||||
return {} as InternalTuonoConfig
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { TuonoConfig } from '../../config'
|
||||
|
||||
import { normalizeConfig } from './normalize-config'
|
||||
|
||||
const PROCESS_CWD_MOCK = 'PROCESS_CWD_MOCK'
|
||||
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(PROCESS_CWD_MOCK)
|
||||
|
||||
describe('normalizeConfig', () => {
|
||||
it('should empty base config if empty config is provided', () => {
|
||||
const config: TuonoConfig = {}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual({
|
||||
server: { host: 'localhost', port: 3000 },
|
||||
vite: { alias: undefined, optimizeDeps: undefined, plugins: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an empty config if invalid values are provided', () => {
|
||||
// @ts-expect-error testing invalid config
|
||||
expect(normalizeConfig({ invalid: true })).toStrictEqual({
|
||||
server: { host: 'localhost', port: 3000 },
|
||||
vite: { alias: undefined, optimizeDeps: undefined, plugins: [] },
|
||||
})
|
||||
})
|
||||
|
||||
describe('server', () => {
|
||||
it('should assign the host and port defined by the user', () => {
|
||||
const config: TuonoConfig = {
|
||||
server: { host: '0.0.0.0', port: 8080 },
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
server: expect.objectContaining({
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('vite - alias', () => {
|
||||
it('should not modify alias pointing to packages', () => {
|
||||
const libraryName = '@tabler/icons-react'
|
||||
const libraryAlias = '@tabler/icons-react/dist/esm/icons/index.mjs'
|
||||
const config: TuonoConfig = {
|
||||
vite: { alias: { [libraryName]: libraryAlias } },
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: {
|
||||
'@tabler/icons-react':
|
||||
'@tabler/icons-react/dist/esm/icons/index.mjs',
|
||||
},
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should transform relative paths to absolute path relative to process.cwd()', () => {
|
||||
const config: TuonoConfig = {
|
||||
vite: { alias: { '@': './src', '@no-prefix': 'src' } },
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: {
|
||||
'@': path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
'@no-prefix': path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
},
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should not transform alias with absolute path', () => {
|
||||
const config: TuonoConfig = {
|
||||
vite: { alias: { '@1': '/src/pippo', '@2': 'file://pluto' } },
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: {
|
||||
'@1': '/src/pippo',
|
||||
'@2': 'file://pluto',
|
||||
},
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should apply previous behavior when using alias as list', () => {
|
||||
const config: TuonoConfig = {
|
||||
vite: {
|
||||
alias: [
|
||||
{ find: '1', replacement: '@tabler/icons-react-fun' },
|
||||
{ find: '2', replacement: './src' },
|
||||
{ find: '3', replacement: 'file://pluto' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: [
|
||||
{
|
||||
find: '1',
|
||||
replacement: '@tabler/icons-react-fun',
|
||||
},
|
||||
{
|
||||
find: '2',
|
||||
replacement: path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
},
|
||||
{
|
||||
find: '3',
|
||||
replacement: 'file://pluto',
|
||||
},
|
||||
],
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import type { AliasOptions } from 'vite'
|
||||
|
||||
import type { TuonoConfig } from '../../config'
|
||||
|
||||
import type { InternalTuonoConfig } from '../types'
|
||||
|
||||
import { DOT_TUONO_FOLDER_NAME, CONFIG_FOLDER_NAME } from '../constants'
|
||||
|
||||
/**
|
||||
* Normalize vite alias option:
|
||||
* - If the path starts with `src` folder, transform it to absolute, prepending the tuono root folder
|
||||
* - If the path is absolute, remove the ".tuono/config/" path from it
|
||||
* - Otherwise leave the path untouched
|
||||
*/
|
||||
const normalizeAliasPath = (aliasPath: string): string => {
|
||||
if (aliasPath.startsWith('./src') || aliasPath.startsWith('src')) {
|
||||
return path.join(process.cwd(), aliasPath)
|
||||
}
|
||||
|
||||
if (path.isAbsolute(aliasPath)) {
|
||||
return aliasPath.replace(
|
||||
path.join(DOT_TUONO_FOLDER_NAME, CONFIG_FOLDER_NAME),
|
||||
'',
|
||||
)
|
||||
}
|
||||
|
||||
return aliasPath
|
||||
}
|
||||
|
||||
/**
|
||||
* From a given vite aliasOptions apply {@link normalizeAliasPath} for each alias.
|
||||
*
|
||||
* The config is bundled by `vite` and emitted inside {@link DOT_TUONO_FOLDER_NAME}/{@link CONFIG_FOLDER_NAME}.
|
||||
* According to this, we have to ensure that the aliases provided by the user are updated to refer to the right folders.
|
||||
*
|
||||
* @see https://github.com/Valerioageno/tuono/pull/153#issuecomment-2508142877
|
||||
*/
|
||||
const normalizeViteAlias = (alias?: AliasOptions): AliasOptions | undefined => {
|
||||
if (!alias) return
|
||||
|
||||
if (Array.isArray(alias)) {
|
||||
return (alias as Extract<AliasOptions, ReadonlyArray<unknown>>).map(
|
||||
({ replacement, ...userAliasDefinition }) => ({
|
||||
...userAliasDefinition,
|
||||
replacement: normalizeAliasPath(replacement),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof alias === 'object') {
|
||||
const normalizedAlias: AliasOptions = {}
|
||||
for (const [key, value] of Object.entries(alias)) {
|
||||
normalizedAlias[key] = normalizeAliasPath(value as string)
|
||||
}
|
||||
return normalizedAlias
|
||||
}
|
||||
|
||||
return alias
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function to normalize the tuono.config.ts file
|
||||
*
|
||||
* @warning Exported for unit test.
|
||||
* There is no easy way to mock the module export and change it in every test
|
||||
* and also testing the error
|
||||
*/
|
||||
export const normalizeConfig = (config: TuonoConfig): InternalTuonoConfig => {
|
||||
return {
|
||||
server: {
|
||||
host: config.server?.host ?? 'localhost',
|
||||
port: config.server?.port ?? 3000,
|
||||
},
|
||||
vite: {
|
||||
alias: normalizeViteAlias(config.vite?.alias),
|
||||
optimizeDeps: config.vite?.optimizeDeps,
|
||||
plugins: config.vite?.plugins ?? [],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export const DOT_TUONO_FOLDER_NAME = '.tuono'
|
||||
export const CONFIG_FOLDER_NAME = 'config'
|
||||
export const CONFIG_FILE_NAME = 'config.mjs'
|
||||
export const SERVER_CONFIG_NAME = 'config.json'
|
||||
|
||||
@@ -6,9 +6,9 @@ import { TuonoFsRouterPlugin } from 'tuono-fs-router-vite-plugin'
|
||||
|
||||
import type { TuonoConfig } from '../config'
|
||||
|
||||
import { loadConfig, blockingAsync } from './utils'
|
||||
import { blockingAsync } from './utils'
|
||||
import { createJsonConfig, loadConfig } from './config'
|
||||
|
||||
const VITE_PORT = 3001
|
||||
const VITE_SSR_PLUGINS: Array<Plugin> = [
|
||||
{
|
||||
enforce: 'post',
|
||||
@@ -110,6 +110,7 @@ const developmentSSRBundle = (): void => {
|
||||
const developmentCSRWatch = (): void => {
|
||||
blockingAsync(async () => {
|
||||
const config = await loadConfig()
|
||||
|
||||
const server = await createServer(
|
||||
mergeConfig<InlineConfig, InlineConfig>(
|
||||
createBaseViteConfigFromTuonoConfig(config),
|
||||
@@ -118,7 +119,8 @@ const developmentCSRWatch = (): void => {
|
||||
base: '/vite-server/',
|
||||
|
||||
server: {
|
||||
port: VITE_PORT,
|
||||
host: config.server.host,
|
||||
port: config.server.port + 1,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
@@ -184,7 +186,7 @@ const buildProd = (): void => {
|
||||
}
|
||||
|
||||
const buildConfig = (): void => {
|
||||
blockingAsync(async () => {
|
||||
blockingAsync(async (): Promise<void> => {
|
||||
await build({
|
||||
root: '.tuono',
|
||||
logLevel: 'silent',
|
||||
@@ -202,6 +204,9 @@ const buildConfig = (): void => {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const config = await loadConfig()
|
||||
await createJsonConfig(config)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { TuonoConfig } from '../config'
|
||||
|
||||
export interface InternalTuonoConfig extends Omit<TuonoConfig, 'server'> {
|
||||
server: Required<NonNullable<TuonoConfig['server']>>
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { TuonoConfig } from '../config'
|
||||
|
||||
import { loadConfig, normalizeConfig } from './utils'
|
||||
|
||||
const PROCESS_CWD_MOCK = 'PROCESS_CWD_MOCK'
|
||||
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(PROCESS_CWD_MOCK)
|
||||
|
||||
describe('loadConfig', () => {
|
||||
it('should error if the config does not exist', async () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined)
|
||||
await loadConfig()
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeConfig - vite', () => {
|
||||
it('should empty base config if empty config is provided', () => {
|
||||
const config: TuonoConfig = {}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual({
|
||||
vite: { alias: undefined, optimizeDeps: undefined, plugins: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an empty config if invalid values are provided', () => {
|
||||
// @ts-expect-error testing invalid config
|
||||
expect(normalizeConfig({ invalid: true })).toStrictEqual({
|
||||
vite: { alias: undefined, optimizeDeps: undefined, plugins: [] },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeConfig - vite - alias', () => {
|
||||
it('should not modify alias pointing to packages', () => {
|
||||
const libraryName = '@tabler/icons-react'
|
||||
const libraryAlias = '@tabler/icons-react/dist/esm/icons/index.mjs'
|
||||
const config: TuonoConfig = {
|
||||
vite: { alias: { [libraryName]: libraryAlias } },
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: {
|
||||
'@tabler/icons-react':
|
||||
'@tabler/icons-react/dist/esm/icons/index.mjs',
|
||||
},
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should transform relative paths to absolute path relative to process.cwd()', () => {
|
||||
const config: TuonoConfig = {
|
||||
vite: { alias: { '@': './src', '@no-prefix': 'src' } },
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: {
|
||||
'@': path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
'@no-prefix': path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
},
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should not transform alias with absolute path', () => {
|
||||
const config: TuonoConfig = {
|
||||
vite: { alias: { '@1': '/src/pippo', '@2': 'file://pluto' } },
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: {
|
||||
'@1': '/src/pippo',
|
||||
'@2': 'file://pluto',
|
||||
},
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should apply previous behavior when using alias as list', () => {
|
||||
const config: TuonoConfig = {
|
||||
vite: {
|
||||
alias: [
|
||||
{ find: '1', replacement: '@tabler/icons-react-fun' },
|
||||
{ find: '2', replacement: './src' },
|
||||
{ find: '3', replacement: 'file://pluto' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
expect(normalizeConfig(config)).toStrictEqual(
|
||||
expect.objectContaining({
|
||||
vite: expect.objectContaining({
|
||||
alias: [
|
||||
{
|
||||
find: '1',
|
||||
replacement: '@tabler/icons-react-fun',
|
||||
},
|
||||
{
|
||||
find: '2',
|
||||
replacement: path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
},
|
||||
{
|
||||
find: '3',
|
||||
replacement: 'file://pluto',
|
||||
},
|
||||
],
|
||||
}) as unknown,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,106 +1,3 @@
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
import type { AliasOptions } from 'vite'
|
||||
|
||||
import type { TuonoConfig } from '../config'
|
||||
|
||||
import {
|
||||
DOT_TUONO_FOLDER_NAME,
|
||||
CONFIG_FOLDER_NAME,
|
||||
CONFIG_FILE_NAME,
|
||||
} from './constants'
|
||||
|
||||
/**
|
||||
* Normalize vite alias option:
|
||||
* - If the path starts with `src` folder, transform it to absolute, prepending the tuono root folder
|
||||
* - If the path is absolute, remove the ".tuono/config/" path from it
|
||||
* - Otherwise leave the path untouched
|
||||
*/
|
||||
const normalizeAliasPath = (aliasPath: string): string => {
|
||||
if (aliasPath.startsWith('./src') || aliasPath.startsWith('src')) {
|
||||
return path.join(process.cwd(), aliasPath)
|
||||
}
|
||||
|
||||
if (path.isAbsolute(aliasPath)) {
|
||||
return aliasPath.replace(
|
||||
path.join(DOT_TUONO_FOLDER_NAME, CONFIG_FOLDER_NAME),
|
||||
'',
|
||||
)
|
||||
}
|
||||
|
||||
return aliasPath
|
||||
}
|
||||
|
||||
/**
|
||||
* From a given vite aliasOptions apply {@link normalizeAliasPath} for each alias.
|
||||
*
|
||||
* The config is bundled by `vite` and emitted inside {@link DOT_TUONO_FOLDER_NAME}/{@link CONFIG_FOLDER_NAME}.
|
||||
* According to this, we have to ensure that the aliases provided by the user are updated to refer to the right folders.
|
||||
*
|
||||
* @see https://github.com/Valerioageno/tuono/pull/153#issuecomment-2508142877
|
||||
*/
|
||||
const normalizeViteAlias = (alias?: AliasOptions): AliasOptions | undefined => {
|
||||
if (!alias) return
|
||||
|
||||
if (Array.isArray(alias)) {
|
||||
return (alias as Extract<AliasOptions, ReadonlyArray<unknown>>).map(
|
||||
({ replacement, ...userAliasDefinition }) => ({
|
||||
...userAliasDefinition,
|
||||
replacement: normalizeAliasPath(replacement),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof alias === 'object') {
|
||||
const normalizedAlias: AliasOptions = {}
|
||||
for (const [key, value] of Object.entries(alias)) {
|
||||
normalizedAlias[key] = normalizeAliasPath(value as string)
|
||||
}
|
||||
return normalizedAlias
|
||||
}
|
||||
|
||||
return alias
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function to normalize the tuono.config.ts file
|
||||
*
|
||||
* @warning Exported for unit test.
|
||||
* There is no easy way to mock the module export and change it in every test
|
||||
* and also testing the error
|
||||
*/
|
||||
export const normalizeConfig = (config: TuonoConfig): TuonoConfig => {
|
||||
return {
|
||||
vite: {
|
||||
alias: normalizeViteAlias(config.vite?.alias),
|
||||
optimizeDeps: config.vite?.optimizeDeps,
|
||||
plugins: config.vite?.plugins ?? [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const loadConfig = async (): Promise<TuonoConfig> => {
|
||||
try {
|
||||
const configFile = (await import(
|
||||
pathToFileURL(
|
||||
path.join(
|
||||
process.cwd(),
|
||||
DOT_TUONO_FOLDER_NAME,
|
||||
CONFIG_FOLDER_NAME,
|
||||
CONFIG_FILE_NAME,
|
||||
),
|
||||
).href
|
||||
)) as { default: TuonoConfig }
|
||||
|
||||
return normalizeConfig(configFile.default)
|
||||
} catch (err) {
|
||||
console.error('Failed to load tuono.config.ts')
|
||||
console.error(err)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export const blockingAsync = (callback: () => Promise<void>): void => {
|
||||
void (async (): Promise<void> => {
|
||||
await callback()
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { AliasOptions, DepOptimizationOptions, PluginOption } from 'vite'
|
||||
|
||||
export interface TuonoConfig {
|
||||
server?: {
|
||||
host?: string
|
||||
port?: number
|
||||
}
|
||||
vite?: {
|
||||
alias?: AliasOptions
|
||||
optimizeDeps?: DepOptimizationOptions
|
||||
|
||||
Reference in New Issue
Block a user