diff --git a/.changeset/shiny-pets-help.md b/.changeset/shiny-pets-help.md new file mode 100644 index 0000000..c189f49 --- /dev/null +++ b/.changeset/shiny-pets-help.md @@ -0,0 +1,8 @@ +--- +"stein-plugin-tailwindcss": minor +"stein-plugin-unocss": minor +"@steinjs/core": minor +"@steinjs/cli": minor +--- + +Add a proper server reload on config update method diff --git a/packages/cli/src/modules/dev.ts b/packages/cli/src/modules/dev.ts index b291d4e..b8b5bf8 100644 --- a/packages/cli/src/modules/dev.ts +++ b/packages/cli/src/modules/dev.ts @@ -1,4 +1,4 @@ -import { type SteinConfig, dev, restartServer } from "@steinjs/core"; +import { type SteinConfig, dev } from "@steinjs/core"; import { watchConfig } from "c12"; import type { Command } from "commander"; @@ -14,12 +14,11 @@ export const devModule = async (options: unknown, command: Command) => { name: "stein", onUpdate: async ({ newConfig: { config } }) => { if (server) { - await restartServer(server, config); + server.container.config = config; + server.container.vite.stein?.restart(); } }, }); server = await dev(cwd, config); - server.printUrls(); - server.bindCLIShortcuts({ print: true }); }; diff --git a/packages/cli/src/utils/createFileWithContent.ts b/packages/cli/src/utils/createFileWithContent.ts index d5112cf..3b8460c 100644 --- a/packages/cli/src/utils/createFileWithContent.ts +++ b/packages/cli/src/utils/createFileWithContent.ts @@ -1,11 +1,11 @@ -import path from "node:path"; -import fs from "node:fs/promises"; - -export const createFileWithContent = async ( - projectDir: string, - fileName: string, - content: string, -) => { - const filePath = path.join(projectDir, fileName); - await fs.writeFile(filePath, content); -}; +import path from "node:path"; +import fs from "node:fs/promises"; + +export const createFileWithContent = async ( + projectDir: string, + fileName: string, + content: string, +) => { + const filePath = path.join(projectDir, fileName); + await fs.writeFile(filePath, content); +}; diff --git a/packages/core/package.json b/packages/core/package.json index 56a9eb8..5b9e9f7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,7 +23,9 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "files": ["dist"], + "files": [ + "dist" + ], "devDependencies": { "@types/node": "^20.14.11", "terser": "^5.31.3", @@ -32,6 +34,7 @@ "typescript": "^5.5.3" }, "dependencies": { + "c12": "^1.11.1", "defu": "^6.1.4", "vite": "^5.3.4", "vite-plugin-solid": "^2.10.2" diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dd32aab..ef480ec 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,24 +4,203 @@ import { type PluginOption as VitePluginOption, build as createViteBuild, createServer as createViteServer, + normalizePath, } from "vite"; import { defu } from "defu"; import type { PartialDeep } from "type-fest"; import solid from "vite-plugin-solid"; +export * from "./utils"; -const sockets = new Set(); +export interface Container { + vite: SteinDevServer; + config: SteinConfig; + restartInFlight: boolean; + close: () => Promise; +} + +export const createContainer = async ( + cwd: string, + config: SteinConfig, +): Promise => { + const viteConfig = await convertToViteConfig(cwd, config); + const vite = (await createViteServer(viteConfig)) as SteinDevServer; + + const container: Container = { + vite, + config, + restartInFlight: false, + close() { + return closeContainer(container); + }, + }; + + return container; +}; + +export const closeContainer = async ({ vite }: Container) => { + await vite.close(); +}; + +export const startContainer = async ({ vite, config }: Container) => { + await vite.listen(config.development.port); + const addr = `http://localhost:${config.development.port}`; + console.log("listening on", addr); // TODO: make this better ? + + return addr; +}; + +const STEIN_CONFIG_RE = /.*stein.config.(?:mjs|cjs|js|ts)$/; +export function shouldRestartContainer( + watchFiles: string[], + restartInFlight: boolean, + changedFile: string, +): boolean { + if (restartInFlight) return false; + const normalizedChangedFile = normalizePath(changedFile); + + // is handled manually in the CLI. + if (STEIN_CONFIG_RE.test(normalizedChangedFile)) return false; + + return watchFiles.some( + (path) => normalizePath(path) === normalizedChangedFile, + ); +} + +async function createRestartedContainer( + cwd: string, + container: Container, +): Promise { + const { config } = container; + const newContainer = await createContainer(cwd, config); + + await startContainer(newContainer); + return newContainer; +} + +export async function restartContainer( + cwd: string, + container: Container, +): Promise { + container.restartInFlight = true; + + try { + await container.close(); + return await createRestartedContainer(cwd, container); + } catch (_err) { + console.error("an error happened", _err); + container.restartInFlight = false; + return _err as Error; + } +} + +interface Restart { + container: Container; + restarted: () => Promise; +} + +export type SteinDevServer = ViteDevServer & { + stein?: { + restart: () => Promise; + watcher: { + add: (pattern: string) => void; + }; + }; +}; + +export const createContainerWithAutomaticRestart = async ( + cwd: string, + config: SteinConfig, +): Promise => { + const initialContainer = await createContainer(cwd, config); + let resolveRestart: (value: Error | null) => void; + let restartComplete = new Promise((resolve) => { + resolveRestart = resolve; + }); -const convertToViteConfig = async ( + let watchFiles: string[] = []; + + const restart: Restart = { + container: initialContainer, + restarted() { + return restartComplete; + }, + }; + + async function handleServerRestart(logMsg = "") { + console.info(`${logMsg} Restarting...`.trim()); + const container = restart.container; + watchFiles = []; // reset, will be filled at restart. + + const result = await restartContainer(cwd, container); + if (result instanceof Error) { + // Failed to restart, use existing container + resolveRestart(result); + } else { + // Restart success. Add new watches because this is a new container with a new Vite server + restart.container = result; + setupContainer(); + resolveRestart(null); + } + restartComplete = new Promise((resolve) => { + resolveRestart = resolve; + }); + } + + function handleChangeRestart(logMsg: string) { + return async (changedFile: string) => { + if ( + shouldRestartContainer( + watchFiles, + restart.container.restartInFlight, + changedFile, + ) + ) { + handleServerRestart(logMsg); + } + }; + } + + // Set up watchers, vite restart API, and shortcuts + function setupContainer() { + const watcher = restart.container.vite.watcher; + watcher.on("change", handleChangeRestart("config file updated.")); + watcher.on("unlink", handleChangeRestart("config file removed.")); + watcher.on("add", handleChangeRestart("config file added.")); + + // Restart the Stein dev server instead of Vite's when the API is called by plugins. + // Ignore the `forceOptimize` parameter for now. + restart.container.vite.stein = { + restart: handleServerRestart, + watcher: { + add: (pattern) => void watchFiles.push(pattern), + }, + }; + + // Set up shortcuts, overriding Vite's default shortcuts so it works for Astro + restart.container.vite.bindCLIShortcuts({ + customShortcuts: [ + // Disable Vite's builtin "r" (restart server), "u" (print server urls) and "c" (clear console) shortcuts + { key: "r", description: "" }, + { key: "u", description: "" }, + { key: "c", description: "" }, + ], + }); + } + setupContainer(); + return restart; +}; + +export const convertToViteConfig = async ( cwd: string, config: SteinConfig, ): Promise => { let solidIndex = 0; const plugins: VitePluginOption = [solid()]; - for (const pluginPromise of config.plugins) { - const plugin = await pluginPromise; + for (const createPlugin of config.plugins) { + const plugin = await createPlugin(); // We need to register the plugins that were made for Vite. for (const vitePlugin of plugin.extends ?? []) { @@ -49,24 +228,16 @@ const convertToViteConfig = async ( }; }; -export const dev = async ( - cwd: string, - config: SteinConfig, -): Promise => { - const server = await createViteServer(await convertToViteConfig(cwd, config)); - - await server.listen(); - server.httpServer?.on("connection", (socket) => { - sockets.add(socket); - console.log("Socket connected"); - - server.httpServer?.on("close", () => { - console.log("Socket deleted"); - sockets.delete(socket); - }); - }); +export const dev = async (cwd: string, config: SteinConfig) => { + const restart = await createContainerWithAutomaticRestart(cwd, config); + const devServerAddressInfo = await startContainer(restart.container); - return server; + return { + address: devServerAddressInfo, + get container() { + return restart.container; + }, + }; }; export const build = async ( @@ -76,40 +247,12 @@ export const build = async ( await createViteBuild(await convertToViteConfig(cwd, config)); }; -export const restartServer = async ( - server: ViteDevServer, - config: SteinConfig, -): Promise => { - await server.close(); - - // For some reason we have to do this. - server.httpServer?.removeAllListeners(); - server.httpServer?.close(); - server.httpServer?.emit("close"); - - //@ts-ignore - server.httpServer = undefined; // Override with undefined to force garbage collection. - - try { - for (const socket of sockets) { - // biome-ignore lint/suspicious/noExplicitAny: - (socket as any).destroy(); - sockets.delete(socket); - } - } catch {} - - // Add a small delay to ensure port is released (spoiler: it's not when this func called from the CLI) - await new Promise((resolve) => setTimeout(resolve, 1000)); - - return await dev(process.cwd(), config); -}; - export interface SteinConfig { /** * Stein plugins to use in the project. * @default [] */ - plugins: Promise[]; + plugins: Array<() => Promise | Plugin>; development: { /** @@ -145,5 +288,6 @@ export interface Plugin { } /** Helper to have types when making a new plugin. */ -export const definePlugin = (plugin: (config?: T) => Promise) => - plugin; +export const definePlugin = ( + plugin: (config?: T) => () => Promise | Plugin, +) => plugin; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts new file mode 100644 index 0000000..24db461 --- /dev/null +++ b/packages/core/src/utils.ts @@ -0,0 +1,124 @@ +import type { ViteDevServer } from "vite"; +import type { SteinDevServer } from "."; +import path from "node:path"; +import fs from "node:fs"; +import { loadConfig } from "c12"; +import defu from "defu"; + +export function until(conditionFunction: () => boolean) { + return new Promise((resolve) => { + void (function check() { + if (conditionFunction()) resolve(); + else setTimeout(check, 100); + })(); + }); +} + +export function waitToAddToWatcher( + filePaths: string[], + server: ViteDevServer, +): void { + queueMicrotask(() => { + until(() => "stein" in server).then(() => { + for (const filePath of filePaths) { + (server as SteinDevServer).stein?.watcher.add(filePath); + } + }); + }); +} + +export const checkFileExists = (path: string) => { + try { + fs.accessSync(path, fs.constants.F_OK); + return true; + } catch { + return false; + } +}; + +export const DEFAULT_JS_EXTENSIONS = [".js", ".ts", ".mjs", ".mts", ".cts"]; + +/** + * @param defaultConfigName base name for the configuration, eg.: `tailwind.config` (without) extension + */ +export const findDefaultConfigFilePath = ( + cwd: string, + defaultConfigName: string, + extensions = DEFAULT_JS_EXTENSIONS, +): string | undefined => { + for (const ext of extensions) { + const configFile = path.join(cwd, `${defaultConfigName}${ext}`); + if (checkFileExists(configFile)) return configFile; + } +}; + +/** + * Reads the configuration script + * and merges with default configuration. + */ +export const readConfigurationScript = async ( + cwd: string, + configName: string, + defaultConfig?: Partial, +): Promise => { + const { config } = await loadConfig({ + cwd, + configFile: configName, + }); + + return defu(config ?? {}, defaultConfig) as T; +}; + +/** + * Provides information to use + * `readConfigurationScript`. + * + * @param configPath relative path of the configuration script, eg.: `configs/tailwind.config.ts` + * @param defaultConfigName base name for the configuration, eg.: `tailwind.config` (without) extension + */ +export const findConfigurationScript = ( + configPath: string, + defaultConfigName: string, +) => { + // No file is present, provide default so C12 can look up for us. + if (!checkFileExists(configPath)) { + return { + configName: defaultConfigName, + cwd: process.cwd(), + }; + } + + const specifiedConfig = path.join(process.cwd(), configPath); + const specifiedConfigPath = path.dirname(specifiedConfig); + const specifiedConfigFileName = path.basename(specifiedConfig); + + // Remove file extension from config file name (.ts, .js, .cjs, etc.) + // C12 will look that up automatically for us. + const specifiedConfigFileNameWithoutExtension = path.basename( + specifiedConfigFileName, + path.extname(specifiedConfigFileName), + ); + + return { + cwd: specifiedConfigPath, + configName: specifiedConfigFileNameWithoutExtension, + }; +}; + +/** + * Provides the full path for a configuration script + * so the vite watcher can hook up to it. + * + * @param configPath relative path of the configuration script, eg.: `configs/tailwind.config.ts` + * @param defaultConfigName base name for the configuration, eg.: `tailwind.config` (without) extension + */ +export const findConfigurationScriptFullPath = ( + configPath: string, + defaultConfigName: string, +): string | undefined => { + if (checkFileExists(configPath)) { + return path.join(process.cwd(), configPath); + } + + return findDefaultConfigFilePath(process.cwd(), defaultConfigName); +}; diff --git a/plugins/tailwindcss/package.json b/plugins/tailwindcss/package.json index e1a2fe6..e4e7059 100644 --- a/plugins/tailwindcss/package.json +++ b/plugins/tailwindcss/package.json @@ -35,7 +35,6 @@ }, "dependencies": { "autoprefixer": "^10.4.19", - "c12": "^1.11.1", "defu": "^6.1.4", "tailwindcss": "^3.4.6" } diff --git a/plugins/tailwindcss/src/index.ts b/plugins/tailwindcss/src/index.ts index 0b785ee..c2c2fe2 100644 --- a/plugins/tailwindcss/src/index.ts +++ b/plugins/tailwindcss/src/index.ts @@ -1,26 +1,29 @@ -import { type Plugin, definePlugin } from "@steinjs/core"; +import { + type Plugin, + definePlugin, + waitToAddToWatcher, + readConfigurationScript, + findConfigurationScript, + findConfigurationScriptFullPath, +} from "@steinjs/core"; + import autoprefixer from "autoprefixer"; import defu from "defu"; -import path from "node:path"; -import fs from "node:fs"; - import tailwindcss from "tailwindcss"; import type { Config as TailwindConfig } from "tailwindcss"; - -import { findConfigFile } from "./utils/findConfigFile"; -import { loadConfig } from "c12"; - -export type SteinTailwindConfig = Partial; +export type { TailwindConfig }; const TW_INJECT_ID = "__stein@tailwindcss.css"; +const TW_DEFAULT_CONFIG_NAME = "tailwind.config"; +const TW_DEFAULT_CONFIG_PATH = TW_DEFAULT_CONFIG_NAME + ".js"; -type Config = { +interface Config { /** * A direct way to change your Tailwind config * (only recommended if you have a very small config, otherwise please use an external config file) */ - config?: Partial; + config: Partial; /** * Override for the path to your Tailwind config file @@ -28,153 +31,92 @@ type Config = { * @default "tailwind.config.js" * @example "configs/tailwind.config.js" */ - configPath?: string; -}; + configPath: string; +} const defaultConfiguration: Config = { config: { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], }, - configPath: "tailwind.config.js", + configPath: TW_DEFAULT_CONFIG_PATH, }; -export default definePlugin((userConfiguration) => { - const mergedSteinConfigs = defu(userConfiguration, defaultConfiguration); +export default definePlugin>( + (userConfiguration) => async () => { + const pluginConfig = defu(userConfiguration, defaultConfiguration); - const localTailwindConfigFile = - getLocalTailwindConfigFile(mergedSteinConfigs); // We need this for getting back the config file that is used to watch for changes + const tailwindConfigFilePath = findConfigurationScriptFullPath( + pluginConfig.configPath, + TW_DEFAULT_CONFIG_NAME, + ); - return { - name: "tailwindcss", - vite: { - name: "stein:tailwindcss", - enforce: "pre", + return { + name: "tailwindcss", + vite: { + name: "stein:tailwindcss", + enforce: "pre", - resolveId(id) { - if (id === TW_INJECT_ID) return id; - }, + resolveId(id) { + if (id === TW_INJECT_ID) return id; + }, - load(id) { - if (id.endsWith(TW_INJECT_ID)) { - return [ - "@tailwind base;", - "@tailwind components;", - "@tailwind utilities;", - ].join("\n"); - } - }, + load(id) { + if (id.endsWith(TW_INJECT_ID)) { + return [ + "@tailwind base;", + "@tailwind components;", + "@tailwind utilities;", + ].join("\n"); + } + }, - config: async () => { - const twConfigResolved = - ((await loadTailwindConfig(mergedSteinConfigs)) as TailwindConfig) ?? - defaultConfiguration.config; + config: async () => { + const tailwindConfig = await readTailwindConfig( + pluginConfig.configPath, + // should merge with inlined config + pluginConfig.config, + ); - return { - css: { - transformer: "postcss", - postcss: { - plugins: [autoprefixer(), tailwindcss(twConfigResolved)], + return { + css: { + transformer: "postcss", + postcss: { + plugins: [autoprefixer(), tailwindcss(tailwindConfig)], + }, }, - }, - }; - }, - - configureServer(server) { - server.watcher.add(localTailwindConfigFile ?? []); - server.watcher.on("add", handleTailwindConfigChange); - server.watcher.on("change", handleTailwindConfigChange); - server.watcher.on("unlink", handleTailwindConfigChange); - - function handleTailwindConfigChange(file: string) { - if (file !== localTailwindConfigFile) return; + }; + }, - server.restart(); // Full server restart to make sure the config is reloaded - } - }, + configureServer: async (server) => { + if (!tailwindConfigFilePath) return; + waitToAddToWatcher([tailwindConfigFilePath], server); + }, - transformIndexHtml: { - order: "pre", - handler: (html) => { - const endHead = html.indexOf(""); - return ( - // biome-ignore lint: better readability - html.slice(0, endHead) + - `` + - html.slice(endHead) - ); + transformIndexHtml: { + order: "pre", + handler: (html) => { + const endHead = html.indexOf(""); + return ( + // biome-ignore lint: better readability + html.slice(0, endHead) + + `` + + html.slice(endHead) + ); + }, }, }, - }, - } satisfies Plugin; -}); - -const loadTailwindConfig = async (steinConfigMerged: Config) => { - const { configFile, cwd } = getLocalConfigInfo(steinConfigMerged); - - // Try to load the local config if any was found - const { config } = await loadConfig({ - cwd, - configFile, - }); - - const tailwindConfig = defu(config ?? {}, steinConfigMerged.config); - - return tailwindConfig; -}; - -const getLocalTailwindConfigFile = (steinConfigMerged: Config) => { - // Check if file specified by user exists - const specifiedConfigFound = checkIfFileExists( - steinConfigMerged.configPath ?? "tailwind.config.js", - ); - - // Use specified config if found, otherwise try to find one in the project folder - const localTailwindConfigFile = specifiedConfigFound - ? path.join(process.cwd(), steinConfigMerged.configPath ?? "") - : findConfigFile(process.cwd()); - - return localTailwindConfigFile; -}; - -const checkIfFileExists = (path: string) => { - try { - fs.accessSync(path, fs.constants.F_OK); - return true; - } catch { - return false; - } -}; - -const getLocalConfigInfo = (steinConfigMerged: Config) => { - // Check if file specified by user exists - const specifiedConfigFound = checkIfFileExists( - steinConfigMerged.configPath ?? "tailwind.config.js", - ); - - if (!specifiedConfigFound) { - // If we don't find our custom file, pass a default taiwind.config name for c12 to search for in the project root - return { - configFile: "tailwind.config", - cwd: process.cwd(), - }; - } - - // Get info about custom config path - const specifiedConfigPath = path.dirname( - path.join(process.cwd(), steinConfigMerged.configPath ?? ""), - ); - const specifiedConfigFileName = path.basename( - path.join(process.cwd(), steinConfigMerged.configPath ?? ""), - ); - - // Remove file extension from config File name (.ts, .js, .cjs, etc.) - const specifiedConfigFileNameWithoutExtension = path.basename( - specifiedConfigFileName, - path.extname(specifiedConfigFileName), + } satisfies Plugin; + }, +); + +const readTailwindConfig = async ( + configPath = TW_DEFAULT_CONFIG_PATH, + inlineConfig: TailwindConfig, +): Promise => { + const { cwd, configName } = findConfigurationScript( + configPath, + TW_DEFAULT_CONFIG_NAME, ); - return { - configFile: specifiedConfigFileNameWithoutExtension, - cwd: specifiedConfigPath, - }; + return readConfigurationScript(cwd, configName, inlineConfig); }; diff --git a/plugins/tailwindcss/src/utils/findConfigFile.ts b/plugins/tailwindcss/src/utils/findConfigFile.ts deleted file mode 100644 index d762263..0000000 --- a/plugins/tailwindcss/src/utils/findConfigFile.ts +++ /dev/null @@ -1,18 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; - -export const SUPPORTED_EXTENSIONS = [".js", ".ts", ".mjs", ".mts", ".cts"]; - -export const findConfigFile = (projectDir: string) => { - for (const ext of SUPPORTED_EXTENSIONS) { - const configFile = path.join(projectDir, `tailwind.config${ext}`); - try { - fs.accessSync(configFile, fs.constants.F_OK); - return configFile; - } catch { - // Ignore error - } - } - console.error("Tailwind config file not found"); - return undefined; -}; diff --git a/plugins/unocss/package.json b/plugins/unocss/package.json index 811b2ac..5a0c9ba 100644 --- a/plugins/unocss/package.json +++ b/plugins/unocss/package.json @@ -34,7 +34,6 @@ "typescript": "^5.5.3" }, "dependencies": { - "c12": "^1.11.1", "defu": "^6.1.4", "unocss": "^0.61.5" } diff --git a/plugins/unocss/src/index.ts b/plugins/unocss/src/index.ts index a170cf3..1c037ae 100644 --- a/plugins/unocss/src/index.ts +++ b/plugins/unocss/src/index.ts @@ -1,22 +1,25 @@ import { type Plugin, - type SteinConfig, definePlugin, - restartServer, + findConfigurationScript, + findConfigurationScriptFullPath, + readConfigurationScript, + waitToAddToWatcher, } from "@steinjs/core"; -import fs from "node:fs"; -import path from "node:path"; import { defu } from "defu"; -import { loadConfig } from "c12"; -import { findConfigFile } from "./utils/findConfigFile"; import unocss from "unocss/vite"; -import type { UserConfig } from "unocss"; +import type { UserConfig as UnoConfig } from "unocss"; -export { defineConfig } from "unocss"; +// Re-export to prevent users to install +// unocss dependency in their project +// for `uno.config.ts`. +export * from "unocss"; const UNO_INJECT_ID = "__stein@unocss"; +const UNO_DEFAULT_CONFIG_NAME = "uno.config"; +const UNO_DEFAULT_CONFIG_PATH = UNO_DEFAULT_CONFIG_NAME + ".ts"; interface Config { /** @@ -24,7 +27,7 @@ interface Config { * * @default true */ - injectEntry?: boolean; + injectEntry: boolean; /** * Include reset styles. @@ -33,20 +36,20 @@ interface Config { * @default false * @example "@unocss/reset/normalize.css" */ - injectReset?: boolean | string; + injectReset: boolean | string; /** * Inject extra imports. * * @default [] */ - injectExtra?: string[]; + injectExtra: string[]; /** * A direct way to change your UnoCSS config * (only recommended if you have a very small config, otherwise please use an external config file) */ - config?: Partial; + config: Partial; /** * Override for the path to your UnoCSS config file @@ -54,159 +57,98 @@ interface Config { * @default "uno.config.ts" * @example "configs/uno.config.ts" */ - configPath?: string; + configPath: string; } const defaultConfiguration: Config = { injectEntry: true, injectReset: false, injectExtra: [], - configPath: "uno.config.ts", + configPath: UNO_DEFAULT_CONFIG_PATH, config: {}, }; -export default definePlugin(async (userConfiguration) => { - const steinConfigMerged = defu( - userConfiguration, - defaultConfiguration, - ) as Config; - const { injectEntry, injectReset, injectExtra } = steinConfigMerged; - const injects = injectExtra ?? []; - - const localUnoConfigFile = getLocalUnoCssConfigFile(steinConfigMerged); // We need this for getting back the config file that is used to watch for changes - - if (injectReset) { - const resetPath = - typeof injectReset === "string" - ? injectReset - : "@unocss/reset/tailwind.css"; - - injects.push(`import ${JSON.stringify(resetPath)}`); - } - - if (injectEntry) { - injects.push('import "uno.css"'); - } - - return { - name: "unocss", - extends: [ - { - position: "before-solid", - plugin: unocss(await loadUnoConfig(steinConfigMerged)), - }, - ], - vite: { - name: "stein:unocss", - enforce: "pre", +export default definePlugin>( + (userConfiguration) => async () => { + const pluginConfig = defu(userConfiguration, defaultConfiguration); - resolveId(id) { - if (id === UNO_INJECT_ID) return id; - }, + const { injectEntry, injectReset, injectExtra } = pluginConfig; + const injects = injectExtra ?? []; - load(id) { - if (id.endsWith(UNO_INJECT_ID)) return injects.join("\n"); - }, + const unoConfigFilePath = findConfigurationScriptFullPath( + pluginConfig.configPath, + UNO_DEFAULT_CONFIG_NAME, + ); - configureServer: async (server) => { - server.watcher.add(localUnoConfigFile ?? []); - server.watcher.on("add", await handleUnoConfigChange); - server.watcher.on("change", await handleUnoConfigChange); - server.watcher.on("unlink", await handleUnoConfigChange); + if (injectReset) { + const resetPath = + typeof injectReset === "string" + ? injectReset + : "@unocss/reset/tailwind.css"; - async function handleUnoConfigChange(file: string) { - if (file !== localUnoConfigFile) return; + injects.push(`import ${JSON.stringify(resetPath)}`); + } - const { config } = await loadConfig({ - cwd: process.cwd(), - name: "stein", - }); + if (injectEntry) { + injects.push('import "uno.css"'); + } - await restartServer(server, config as SteinConfig); - } - }, - - transformIndexHtml: { - enforce: "pre", - handler: (html) => { - const endHead = html.indexOf(""); - return ( - // biome-ignore lint: better readability - html.slice(0, endHead) + - `` + - html.slice(endHead) - ); + return { + name: "unocss", + extends: [ + { + position: "before-solid", + plugin: unocss( + await readUnoConfig( + pluginConfig.configPath, + // should merge with inlined config + pluginConfig.config, + ), + ), }, - }, - }, - } satisfies Plugin; -}); - -const loadUnoConfig = async (steinConfigMerged: Partial) => { - const { configFile, cwd } = getLocalConfigInfo(steinConfigMerged); - - // Try to load the local config if any was found - const { config } = await loadConfig({ - cwd, - configFile, - }); - - const unoConfig = defu(config ?? {}, steinConfigMerged.config); - - return unoConfig; -}; - -const getLocalUnoCssConfigFile = (steinConfigMerged: Partial) => { - // Check if file specified by user exists - const specifiedConfigFound = checkIfFileExists(steinConfigMerged.configPath); - - // Use specified config if found, otherwise try to find one in the project folder - const localTailwindConfigFile = specifiedConfigFound - ? path.join(process.cwd(), steinConfigMerged.configPath ?? "") - : findConfigFile(process.cwd()); - - return localTailwindConfigFile; -}; - -const checkIfFileExists = (path: string | undefined) => { - if (!path) return false; + ], + vite: { + name: "stein:unocss", + enforce: "pre", - try { - fs.accessSync(path, fs.constants.F_OK); - return true; - } catch { - return false; - } -}; + resolveId(id) { + if (id === UNO_INJECT_ID) return id; + }, -const getLocalConfigInfo = (steinConfigMerged: Partial) => { - // Check if file specified by user exists - const specifiedConfigFound = checkIfFileExists(steinConfigMerged.configPath); + load(id) { + if (id.endsWith(UNO_INJECT_ID)) return injects.join("\n"); + }, - if (!specifiedConfigFound) { - // If we don't find our custom file, pass a default uno.config name for c12 to search for in the project root - return { - configFile: "uno.config", - cwd: process.cwd(), - }; - } - - // Get info about custom config path - const specifiedConfigPath = path.dirname( - path.join(process.cwd(), steinConfigMerged.configPath ?? ""), - ); - const specifiedConfigFileName = path.basename( - path.join(process.cwd(), steinConfigMerged.configPath ?? ""), - ); + configureServer: async (server) => { + if (!unoConfigFilePath) return; + waitToAddToWatcher([unoConfigFilePath], server); + }, - // Remove file extension from config File name (.ts, .js, .cjs, etc.) - const specifiedConfigFileNameWithoutExtension = path.basename( - specifiedConfigFileName, - path.extname(specifiedConfigFileName), + transformIndexHtml: { + order: "pre", + handler: (html) => { + const endHead = html.indexOf(""); + return ( + // biome-ignore lint: better readability + html.slice(0, endHead) + + `` + + html.slice(endHead) + ); + }, + }, + }, + } satisfies Plugin; + }, +); + +const readUnoConfig = async ( + configPath = UNO_DEFAULT_CONFIG_PATH, + inlineConfig: UnoConfig, +): Promise => { + const { cwd, configName } = findConfigurationScript( + configPath, + UNO_DEFAULT_CONFIG_NAME, ); - return { - configFile: specifiedConfigFileNameWithoutExtension, - cwd: specifiedConfigPath, - }; + return readConfigurationScript(cwd, configName, inlineConfig); }; diff --git a/plugins/unocss/src/utils/findConfigFile.ts b/plugins/unocss/src/utils/findConfigFile.ts deleted file mode 100644 index ad16a94..0000000 --- a/plugins/unocss/src/utils/findConfigFile.ts +++ /dev/null @@ -1,18 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; - -export const SUPPORTED_EXTENSIONS = [".js", ".ts", ".mjs", ".mts", ".cts"]; - -export const findConfigFile = (projectDir: string) => { - for (const ext of SUPPORTED_EXTENSIONS) { - const configFile = path.join(projectDir, `uno.config${ext}`); - try { - fs.accessSync(configFile, fs.constants.F_OK); - return configFile; - } catch { - // Ignore error - } - } - console.error("Tailwind config file not found"); - return undefined; -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0629814..6b06175 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,7 +57,7 @@ importers: dependencies: '@unocss/reset': specifier: latest - version: 0.61.5 + version: 0.61.6 solid-js: specifier: ^1.8.18 version: 1.8.18 @@ -120,6 +120,9 @@ importers: packages/core: dependencies: + c12: + specifier: ^1.11.1 + version: 1.11.1(magicast@0.3.4) defu: specifier: ^6.1.4 version: 6.1.4 @@ -167,9 +170,6 @@ importers: autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.39) - c12: - specifier: ^1.11.1 - version: 1.11.1(magicast@0.3.4) defu: specifier: ^6.1.4 version: 6.1.4 @@ -192,9 +192,6 @@ importers: plugins/unocss: dependencies: - c12: - specifier: ^1.11.1 - version: 1.11.1(magicast@0.3.4) defu: specifier: ^6.1.4 version: 6.1.4 @@ -1017,6 +1014,9 @@ packages: '@unocss/reset@0.61.5': resolution: {integrity: sha512-5FKNsHnke9J1Z0T4prOZn9hkWh86c6Px+Oh3xf8mDd6dDw8CjzYMRxZEKti0gt13NcsO29G1vLGM7UjG1sCamg==} + '@unocss/reset@0.61.6': + resolution: {integrity: sha512-fAlE2EbG5h6yS6oIBDOKw4VHogLcl/Ao9EbrcNe1uRc79cA1rvSkqeMAxLHvGxwXAzu7LIovknR+pzASeVLZBg==} + '@unocss/rule-utils@0.61.5': resolution: {integrity: sha512-sCHnpCQoj3/ZmCjYo+oW3+4r5Z8kFI2snEL+miU2Uk0SqCgY1k0cUIYivj5L9ghp29p8VjEusX9M01QEZOYK7g==} engines: {node: '>=14'} @@ -3152,6 +3152,8 @@ snapshots: '@unocss/reset@0.61.5': {} + '@unocss/reset@0.61.6': {} + '@unocss/rule-utils@0.61.5': dependencies: '@unocss/core': 0.61.5