@maizzle/framework
Version:
Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.
136 lines (135 loc) • 5.3 kB
JavaScript
import { defaults } from "./defaults.js";
import { defineConfig } from "../composables/defineConfig.js";
import { existsSync, readFileSync } from "node:fs";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dirname, extname, join, resolve } from "pathe";
import { createJiti } from "jiti";
import { createDefu } from "defu";
//#region src/config/index.ts
const merge = createDefu((obj, key, value) => {
if (Array.isArray(obj[key])) {
obj[key] = value;
return true;
}
});
const CONFIG_FILES = ["maizzle.config.ts", "maizzle.config.js"];
/**
* Resolve the Maizzle config.
*
* Always loads from the config file on disk (maizzle.config.{ts,js}),
* then merges the programmatic config on top, then fills in defaults.
*/
async function resolveConfig(config, cwd = process.cwd()) {
const fileConfig = await loadConfig(typeof config === "string" ? config : void 0, cwd);
return normalizeConfig(typeof config === "object" && config !== null ? config : {}, fileConfig, cwd);
}
/**
* Resolve config from a programmatic object only — never touches disk.
*
* `render()` uses this so a stray `maizzle.config.{ts,js}` in `cwd` can't
* silently alter output: the config you pass is the config you get, layered
* over defaults. Callers who want the project's file config load it
* explicitly via {@link resolveConfig} and pass the result in.
*/
function resolveConfigObject(config, cwd = process.cwd()) {
return normalizeConfig(config && typeof config === "object" ? config : {}, {}, cwd);
}
/**
* Merge programmatic config over file config over defaults, then resolve
* filesystem paths (root, content, static, components) relative to cwd/root.
*/
function normalizeConfig(programmaticConfig, fileConfig, cwd) {
const merged = merge(programmaticConfig, fileConfig, defaults);
const hasExplicitRoot = !!(programmaticConfig.root ?? fileConfig.root);
const root = resolve(cwd, merged.root ?? ".");
merged.root = root;
if (merged.content) merged.content = merged.content.map((p) => {
if (p.startsWith("/") || p.startsWith("!")) return p;
return resolve(root, p);
});
if (merged.static?.source) merged.static.source = merged.static.source.map((p) => {
if (p.startsWith("/") || p.startsWith("!")) return p;
return resolve(root, p);
});
/**
* Resolve components.source paths relative to cwd (not root), since extra
* component dirs often live outside the root directory. String
* entries → resolve in-place. Object entries → resolve
* `path`, preserve `prefix`/`pathPrefix`.
*/
if (merged.components?.source) {
const dirs = Array.isArray(merged.components.source) ? merged.components.source : [merged.components.source];
merged.components.source = dirs.map((entry) => {
if (typeof entry === "string") return entry.startsWith("/") ? entry : resolve(cwd, entry);
return {
...entry,
path: entry.path.startsWith("/") ? entry.path : resolve(cwd, entry.path)
};
});
}
/**
* Default css.base to root when root is explicitly set, so Tailwind resolves
* @source from the right directory. When root is not set, leave
* css.base undefined so Tailwind uses its own default (the
* template file's directory).
*/
if (hasExplicitRoot && !merged.css?.base) {
if (!merged.css) merged.css = {};
merged.css.base = root;
}
return merged;
}
/**
* Whether Node can natively `import()` this file as ESM — true for `.mjs`
* and for `.js` whose nearest package.json declares `"type": "module"`.
* jiti hands such files to Node's native loader, which caches them in the
* (unbustable) module registry, so we import them ourselves with a fresh
* query instead. Everything else (CJS, TS) goes through jiti, which already
* re-evaluates per instance.
*/
function isNativeESM(absolutePath) {
const ext = extname(absolutePath);
if (ext === ".mjs") return true;
if (ext === ".cjs" || ext === ".ts" || ext === ".mts" || ext === ".cts") return false;
let dir = dirname(absolutePath);
for (;;) {
const pkgPath = join(dir, "package.json");
if (existsSync(pkgPath)) try {
return JSON.parse(readFileSync(pkgPath, "utf-8")).type === "module";
} catch {
return false;
}
const parent = dirname(dir);
if (parent === dir) return false;
dir = parent;
}
}
/**
* Monotonic nonce appended to native-import URLs so each load is fresh even
* when two reloads land in the same millisecond.
*/
let configLoadNonce = 0;
async function importConfig(absolutePath, jiti) {
if (isNativeESM(absolutePath)) {
const mod = await import(`${pathToFileURL(absolutePath).href}?t=${Date.now()}-${configLoadNonce++}`);
return mod.default ?? mod;
}
const mod = await jiti.import(absolutePath);
return mod.default ?? mod;
}
async function loadConfig(configPath, cwd = process.cwd()) {
const jiti = createJiti(fileURLToPath(import.meta.url), { moduleCache: false });
if (configPath) {
const absolutePath = resolve(cwd, configPath);
if (!existsSync(absolutePath)) throw new Error(`Config file not found: ${absolutePath}`);
return importConfig(absolutePath, jiti);
}
for (const filename of CONFIG_FILES) {
const filepath = resolve(cwd, filename);
if (existsSync(filepath)) return importConfig(filepath, jiti);
}
return {};
}
//#endregion
export { defaults, defineConfig, isNativeESM, resolveConfig, resolveConfigObject };
//# sourceMappingURL=index.js.map