feat: add tuono.config.ts support (#153)

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
This commit is contained in:
Valerio Ageno
2024-11-30 19:58:04 +01:00
committed by GitHub
parent 8052de8ad5
commit ec4577c187
19 changed files with 411 additions and 78 deletions
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { buildConfig } from '../dist/esm/build/index.js'
buildConfig()
+15 -2
View File
@@ -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",
+3
View File
@@ -0,0 +1,3 @@
export const DOT_TUONO_FOLDER_NAME = '.tuono'
export const CONFIG_FOLDER_NAME = 'config'
export const CONFIG_FILE_NAME = 'config.mjs'
+114 -69
View File
@@ -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 }
+112
View File
@@ -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',
},
],
},
})
})
})
+104
View File
@@ -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()
})()
}
+1
View File
@@ -0,0 +1 @@
export type { TuonoConfig } from './types'
+7
View File
@@ -0,0 +1,7 @@
import type { AliasOptions } from 'vite'
export interface TuonoConfig {
vite?: {
alias?: AliasOptions
}
}
+1
View File
@@ -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',
],