diff --git a/index.d.ts b/index.d.ts index 52b3885..163c232 100644 --- a/index.d.ts +++ b/index.d.ts @@ -5,7 +5,13 @@ declare interface BuildOptions { force?: boolean; /** inline images imported in css as data url even if `bundle` is false */ forceInlineImages?: boolean; - emitDeclarationFile?: boolean; + /** + * emit typescript declaration file for css modules class names + * - `.css.d.ts` : emit `xxx.css.d.ts` + * - `.d.css.ts` : emit `xxx.d.css.ts` (from typescript@5, see https://www.typescriptlang.org/tsconfig#allowArbitraryExtensions) + * - `true` : emit both `xxx.css.d.ts` and `xxx.d.css.ts` + */ + emitDeclarationFile?: boolean | '.d.css.ts' | '.css.d.ts'; inject?: boolean | string | ((css: string, digest: string) => string); filter?: RegExp; /** @@ -52,8 +58,8 @@ declare interface BuildOptions { declare function CssModulesPlugin(options?: BuildOptions): Plugin; declare namespace CssModulesPlugin { - export interface Options extends BuildOptions {}; - + export interface Options extends BuildOptions {} + export interface BuildContext { options: Options; buildId: string; diff --git a/index.js b/index.js index 0ce7084..990e7ff 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ import { validateOptions } from './lib/utils.js'; import { compact } from 'lodash-es'; -import { readFile, writeFile } from 'node:fs/promises'; +import { readFile, rename, writeFile } from 'node:fs/promises'; import { patchContext } from './lib/context.js'; /** @@ -39,7 +39,7 @@ export const setup = (build, _options) => { const jsLoader = patchedBuild.initialOptions.loader?.['.js'] ?? 'js'; const outJsExt = patchedBuild.initialOptions.outExtension?.['.js'] ?? '.js'; const forceInlineImages = !!options.forceInlineImages; - const emitDts = !!options.emitDeclarationFile; + const emitDts = options.emitDeclarationFile; patchedBuild.onLoad({ filter: /.+/, namespace: pluginCssNamespace }, (args) => { const { path } = args; @@ -85,42 +85,52 @@ export const setup = (build, _options) => { ); patchedBuild.onLoad({ filter: modulesCssRegExp, namespace: 'file' }, async (args) => { + if (!emitDts && !bundle && !forceBuild) { + return undefined; + } + log('[file] on load:', args); const { path } = args; const rpath = relative(buildRoot, path); - const prefix = basename(rpath, extname(path)) .replace(/[^a-zA-Z0-9]/g, '-') .replace(/^\-*/, ''); const suffix = patchedBuild.context.packageVersion?.replace(/[^a-zA-Z0-9]/g, '') ?? ''; - CSSTransformer.getInstance(patchedBuild).bundle(path, { + + const buildResult = CSSTransformer.getInstance(patchedBuild).bundle(path, { prefix, suffix, forceInlineImages, - emitDeclarationFile: emitDts + emitDeclarationFile: !!emitDts }); - if (!bundle && !forceBuild) { - return undefined; - } else if (!bundle && forceBuild) { - log('force build modules css:', rpath); - const buildResult = CSSTransformer.getInstance(patchedBuild).getCachedResult(path); - - if (emitDts) { - const outdir = resolve(buildRoot, patchedBuild.initialOptions.outdir ?? ''); - const outbase = patchedBuild.initialOptions.outbase; - let outDtsfile = resolve(outdir, rpath) + '.d.ts'; + if (emitDts) { + const dtsExts = []; + if (emitDts === '.d.css.ts' || emitDts === '.css.d.ts') { + dtsExts.push(emitDts); + } else { + dtsExts.push('.d.css.ts', '.css.d.ts'); + } + const outdir = resolve(buildRoot, patchedBuild.initialOptions.outdir ?? ''); + const outbase = patchedBuild.initialOptions.outbase; + dtsExts.forEach(async (dtsExt) => { + let outDtsfile = resolve(outdir, rpath).replace(/\.css$/i, dtsExt); if (outbase) { let normalized = normalize(outbase); if (normalized.endsWith(sep)) { normalized = compact(normalized.split(sep)).join(sep); } - outDtsfile = resolve(outDtsfile.replace(normalized, '')); + if (normalized !== '.') { + outDtsfile = resolve(outDtsfile.replace(normalized, '')); + } } + log(`emit typescript declarations file:`, patchedBuild.context.relative(outDtsfile)); + await ensureFile(outDtsfile, buildResult?.dts ?? ''); + }); + } - ensureFile(outDtsfile, buildResult?.dts ?? ''); - } - + if (!bundle && forceBuild) { + log('force build modules css:', rpath); if (injectCss) { const anotherBuildOptions = { ...patchedBuild.initialOptions }; delete anotherBuildOptions.entryPoints; @@ -192,11 +202,10 @@ export const setup = (build, _options) => { }; } } else if (bundle) { - const bundleResult = CSSTransformer.getInstance(patchedBuild).getCachedResult(path); return { - contents: bundleResult?.js, + contents: buildResult?.js, loader: jsLoader, - watchFiles: [path, ...(bundleResult?.composedFiles ?? [])], + watchFiles: [path, ...(buildResult?.composedFiles ?? [])], resolveDir: dirname(path), pluginData: { originCssPath: path @@ -214,8 +223,14 @@ export const setup = (build, _options) => { if (!bundle && forceBuild) { /** @type {[string, Record][]} */ const jsFiles = []; - /** @type {Record} */ + /** @type {[string, string][]} */ + const moduleJsFiles = []; + Object.entries(r.metafile?.outputs ?? {}).forEach(([js, meta]) => { + if (meta.entryPoint && modulesCssRegExp.test(meta.entryPoint)) { + moduleJsFiles.push([meta.entryPoint, js]); + } + if (meta.entryPoint && !modulesCssRegExp.test(meta.entryPoint)) { let shouldPush = false; /** @type {Record} */ @@ -223,8 +238,7 @@ export const setup = (build, _options) => { meta.imports?.forEach((imp) => { if (modulesCssRegExp.test(imp.path)) { shouldPush = true; - const impExt = extname(imp.path); - defines[imp.path] = imp.path.replace(new RegExp(`${impExt}$`), `${outJsExt}`); + defines[imp.path] = imp.path + outJsExt; } }); if (shouldPush) { @@ -233,8 +247,15 @@ export const setup = (build, _options) => { } }); - await Promise.all( - jsFiles.map(([js, places]) => { + await Promise.all([ + ...(moduleJsFiles.map(([src, dist]) => { + const fp = resolve(buildRoot, dist); + const filename = basename(src) + outJsExt; + const finalPath = resolve(dirname(fp), filename); + log(`rename ${dist} to ${filename}`); + return rename(fp, finalPath); + })), + ...jsFiles.map(([js, places]) => { const fulljs = resolve(buildRoot, js); return readFile(fulljs, { encoding: 'utf8' }) .then((content) => { @@ -249,7 +270,7 @@ export const setup = (build, _options) => { return writeFile(fulljs, nc, { encoding: 'utf8' }); }); }) - ); + ]); return dispose(); } diff --git a/lib/utils.js b/lib/utils.js index 2645195..2ccc568 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -3,7 +3,7 @@ import { createHash } from 'node:crypto'; import { accessSync, constants } from 'node:fs'; import { createRequire } from 'node:module'; import { omit } from 'lodash-es'; -import { appendFile, mkdir } from 'node:fs/promises'; +import { writeFile, mkdir } from 'node:fs/promises'; const require = createRequire(import.meta.url); @@ -213,13 +213,13 @@ const simpleMinifyCss = ( * @param {string} filepath * @param {string} data */ -const ensureFile = async (filepath, data) => { - if (!data) { +const ensureFile = async (filepath, data = '') => { + if (!filepath) { return; } const dir = dirname(filepath); await mkdir(dir, { recursive: true }); - await appendFile(filepath, data.trim(), { encoding: 'utf8' }); + await writeFile(filepath, `${data}`.trim(), { encoding: 'utf8' }); }; export { diff --git a/package.json b/package.json index e9cac5f..33860a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "esbuild-css-modules-plugin", - "version": "3.0.0-dev.19", + "version": "3.0.0-dev.20", "description": "A esbuild plugin to bundle css modules into js(x)/ts(x).", "main": "./index.cjs", "module": "./index.js", @@ -34,13 +34,13 @@ "devDependencies": { "@types/lodash-es": "^4.17.7", "@types/node": "^17.0.23", - "esbuild": "^0.18.19" + "esbuild": "^0.19.2" }, "peerDependencies": { "esbuild": "*" }, "dependencies": { - "lightningcss": "^1.21.5", + "lightningcss": "^1.21.7", "lodash": "^4.17.21", "lodash-es": "^4.17.21" },