Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add host and port support to TuonoConfig #366

Merged
merged 7 commits into from
Jan 20, 2025
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
47 changes: 47 additions & 0 deletions packages/tuono/src/build/config/create-json-config.ts
Original file line number Diff line number Diff line change
@@ -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')
}
2 changes: 2 additions & 0 deletions packages/tuono/src/build/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { loadConfig } from './load-config'
export { createJsonConfig } from './create-json-config'
14 changes: 14 additions & 0 deletions packages/tuono/src/build/config/load-config.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
35 changes: 35 additions & 0 deletions packages/tuono/src/build/config/load-config.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
135 changes: 135 additions & 0 deletions packages/tuono/src/build/config/normalize-config.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
)
})
})
})
82 changes: 82 additions & 0 deletions packages/tuono/src/build/config/normalize-config.ts
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions packages/tuono/src/build/constants.ts
Original file line number Diff line number Diff line change
@@ -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'
13 changes: 9 additions & 4 deletions packages/tuono/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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),
Expand All @@ -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: {
Expand Down Expand Up @@ -184,7 +186,7 @@ const buildProd = (): void => {
}

const buildConfig = (): void => {
blockingAsync(async () => {
blockingAsync(async (): Promise<void> => {
await build({
root: '.tuono',
logLevel: 'silent',
Expand All @@ -202,6 +204,9 @@ const buildConfig = (): void => {
},
},
})

const config = await loadConfig()
await createJsonConfig(config)
})
}

Expand Down
5 changes: 5 additions & 0 deletions packages/tuono/src/build/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { TuonoConfig } from '../config'

export interface InternalTuonoConfig extends Omit<TuonoConfig, 'server'> {
server: Required<NonNullable<TuonoConfig['server']>>
}
Loading
Loading