mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: add tuono.config.ts support (#153)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { buildConfig } from '../dist/esm/build/index.js'
|
||||
|
||||
buildConfig()
|
||||
@@ -9,7 +9,9 @@
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||
"types": "tsc --noEmit"
|
||||
"types": "tsc --noEmit",
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -31,6 +33,16 @@
|
||||
"default": "./dist/cjs/build/index.js"
|
||||
}
|
||||
},
|
||||
"./config": {
|
||||
"import": {
|
||||
"types": "./dist/esm/config/index.d.ts",
|
||||
"default": "./dist/esm/config/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/config/index.d.ts",
|
||||
"default": "./dist/cjs/config/index.js"
|
||||
}
|
||||
},
|
||||
"./ssr": {
|
||||
"import": {
|
||||
"types": "./dist/esm/ssr/index.d.ts",
|
||||
@@ -66,7 +78,8 @@
|
||||
"bin": {
|
||||
"tuono-dev-ssr": "./bin/dev-ssr.js",
|
||||
"tuono-dev-watch": "./bin/watch.js",
|
||||
"tuono-build-prod": "./bin/build-prod.js"
|
||||
"tuono-build-prod": "./bin/build-prod.js",
|
||||
"tuono-build-config": "./bin/build-config.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const DOT_TUONO_FOLDER_NAME = '.tuono'
|
||||
export const CONFIG_FOLDER_NAME = 'config'
|
||||
export const CONFIG_FILE_NAME = 'config.mjs'
|
||||
@@ -1,8 +1,11 @@
|
||||
import { build, createServer, InlineConfig } from 'vite'
|
||||
import { build, createServer, InlineConfig, mergeConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import ViteFsRouter from 'tuono-fs-router-vite-plugin'
|
||||
import { LazyLoadingPlugin } from 'tuono-lazy-fn-vite-plugin'
|
||||
import mdx from '@mdx-js/rollup'
|
||||
import { loadConfig, blockingAsync } from './utils'
|
||||
|
||||
const VITE_PORT = 3001
|
||||
|
||||
const BASE_CONFIG: InlineConfig = {
|
||||
root: '.tuono',
|
||||
@@ -22,91 +25,133 @@ const BASE_CONFIG: InlineConfig = {
|
||||
],
|
||||
}
|
||||
|
||||
const VITE_PORT = 3001
|
||||
|
||||
export function developmentSSRBundle() {
|
||||
;(async () => {
|
||||
await build({
|
||||
...BASE_CONFIG,
|
||||
build: {
|
||||
ssr: true,
|
||||
minify: false,
|
||||
outDir: 'server',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/server-main.tsx',
|
||||
// Silent all logs
|
||||
onLog() {},
|
||||
output: {
|
||||
entryFileNames: 'dev-server.js',
|
||||
format: 'iife',
|
||||
const developmentSSRBundle = () => {
|
||||
blockingAsync(async () => {
|
||||
const config = await loadConfig()
|
||||
await build(
|
||||
mergeConfig(BASE_CONFIG, {
|
||||
resolve: {
|
||||
alias: config.vite?.alias || {},
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
minify: false,
|
||||
outDir: 'server',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/server-main.tsx',
|
||||
// Silent all logs
|
||||
onLog() {},
|
||||
output: {
|
||||
entryFileNames: 'dev-server.js',
|
||||
format: 'iife',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
target: 'webworker',
|
||||
noExternal: true,
|
||||
},
|
||||
})
|
||||
})()
|
||||
ssr: {
|
||||
target: 'webworker',
|
||||
noExternal: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function developmentCSRWatch() {
|
||||
;(async () => {
|
||||
const server = await createServer({
|
||||
...BASE_CONFIG,
|
||||
// Entry point for the development vite proxy
|
||||
base: '/vite-server/',
|
||||
|
||||
server: {
|
||||
port: VITE_PORT,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/client-main.tsx',
|
||||
const developmentCSRWatch = () => {
|
||||
blockingAsync(async () => {
|
||||
const config = await loadConfig()
|
||||
const server = await createServer(
|
||||
mergeConfig(BASE_CONFIG, {
|
||||
resolve: {
|
||||
alias: config.vite?.alias || {},
|
||||
},
|
||||
},
|
||||
})
|
||||
// Entry point for the development vite proxy
|
||||
base: '/vite-server/',
|
||||
|
||||
server: {
|
||||
port: VITE_PORT,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/client-main.tsx',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
await server.listen()
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
export function buildProd() {
|
||||
;(async () => {
|
||||
await build({
|
||||
...BASE_CONFIG,
|
||||
build: {
|
||||
manifest: true,
|
||||
emptyOutDir: true,
|
||||
outDir: '../out/client',
|
||||
rollupOptions: {
|
||||
input: './.tuono/client-main.tsx',
|
||||
},
|
||||
},
|
||||
})
|
||||
const buildProd = () => {
|
||||
blockingAsync(async () => {
|
||||
const config = await loadConfig()
|
||||
|
||||
await build(
|
||||
mergeConfig(BASE_CONFIG, {
|
||||
resolve: {
|
||||
alias: config.vite?.alias || {},
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
emptyOutDir: true,
|
||||
outDir: '../out/client',
|
||||
rollupOptions: {
|
||||
input: './.tuono/client-main.tsx',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
await build(
|
||||
mergeConfig(BASE_CONFIG, {
|
||||
resolve: {
|
||||
alias: config.vite?.alias || {},
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
minify: true,
|
||||
outDir: '../out/server',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/server-main.tsx',
|
||||
output: {
|
||||
entryFileNames: 'prod-server.js',
|
||||
format: 'iife',
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
target: 'webworker',
|
||||
noExternal: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const buildConfig = () => {
|
||||
blockingAsync(async () => {
|
||||
await build({
|
||||
...BASE_CONFIG,
|
||||
root: '.tuono',
|
||||
logLevel: 'silent',
|
||||
cacheDir: 'cache',
|
||||
envDir: '../',
|
||||
build: {
|
||||
ssr: true,
|
||||
minify: true,
|
||||
outDir: '../out/server',
|
||||
outDir: 'config',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: './.tuono/server-main.tsx',
|
||||
input: './tuono.config.ts',
|
||||
output: {
|
||||
entryFileNames: 'prod-server.js',
|
||||
format: 'iife',
|
||||
entryFileNames: 'config.mjs',
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
target: 'webworker',
|
||||
noExternal: true,
|
||||
},
|
||||
})
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
export { buildProd, buildConfig, developmentCSRWatch, developmentSSRBundle }
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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(() => {})
|
||||
await loadConfig()
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeConfig - vite - alias', () => {
|
||||
it('should return the config as is, adding `vite` with alias', () => {
|
||||
const config: TuonoConfig = {}
|
||||
expect(normalizeConfig(config)).toStrictEqual({
|
||||
vite: { alias: undefined },
|
||||
})
|
||||
})
|
||||
|
||||
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 },
|
||||
})
|
||||
})
|
||||
|
||||
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({
|
||||
vite: {
|
||||
alias: {
|
||||
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
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({
|
||||
vite: {
|
||||
alias: {
|
||||
'@': path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
'@no-prefix': path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should not transform alias with absolute path', () => {
|
||||
const config: TuonoConfig = {
|
||||
vite: { alias: { '@1': '/src/pippo', '@2': 'file://pluto' } },
|
||||
}
|
||||
expect(normalizeConfig(config)).toStrictEqual({
|
||||
vite: {
|
||||
alias: {
|
||||
'@1': '/src/pippo',
|
||||
'@2': 'file://pluto',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should apply previuos behaviuor 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({
|
||||
vite: {
|
||||
alias: [
|
||||
{
|
||||
find: '1',
|
||||
replacement: '@tabler/icons-react-fun',
|
||||
},
|
||||
{
|
||||
find: '2',
|
||||
replacement: path.join(PROCESS_CWD_MOCK, 'src'),
|
||||
},
|
||||
{
|
||||
find: '3',
|
||||
replacement: 'file://pluto',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import path from 'path'
|
||||
|
||||
import type { AliasOptions } from 'vite'
|
||||
|
||||
import type { TuonoConfig } from '../config'
|
||||
|
||||
import {
|
||||
DOT_TUONO_FOLDER_NAME,
|
||||
CONFIG_FOLDER_NAME,
|
||||
CONFIG_FILE_NAME,
|
||||
} from './constants'
|
||||
import { pathToFileURL } from 'url'
|
||||
|
||||
/**
|
||||
* 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.map(({ replacement, ...userAliasDefinition }) => ({
|
||||
...userAliasDefinition,
|
||||
replacement: normalizeAliasPath(replacement),
|
||||
}))
|
||||
}
|
||||
|
||||
if (typeof alias === 'object') {
|
||||
let normalizedAlias: AliasOptions = {}
|
||||
for (let [key, value] of Object.entries(alias)) {
|
||||
normalizedAlias[key] = normalizeAliasPath(value)
|
||||
}
|
||||
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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
return normalizeConfig(configFile.default)
|
||||
} catch (err) {
|
||||
console.error('Failed to load tuono.config.ts')
|
||||
console.error(err)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export const blockingAsync = (callback: () => Promise<void>) => {
|
||||
;(async () => {
|
||||
await callback()
|
||||
})()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type { TuonoConfig } from './types'
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { AliasOptions } from 'vite'
|
||||
|
||||
export interface TuonoConfig {
|
||||
vite?: {
|
||||
alias?: AliasOptions
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export default mergeConfig(
|
||||
entry: [
|
||||
'./src/index.ts',
|
||||
'./src/build/index.ts',
|
||||
'./src/config/index.ts',
|
||||
'./src/ssr/index.tsx',
|
||||
'./src/hydration/index.tsx',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user