esm-styles 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/lib/build.js +44 -15
  2. package/package.json +1 -1
package/dist/lib/build.js CHANGED
@@ -39,6 +39,33 @@ function createAliasPlugin(aliases, sourcePath) {
39
39
  function escapeRegex(str) {
40
40
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
41
  }
42
+ /**
43
+ * Write a file atomically, skipping the write when the on-disk content already
44
+ * matches. Reads the current file first; if it is identical there is no write,
45
+ * no temp file, and no inode swap, so file watchers (Vite, Cursor) stay quiet
46
+ * and no needless HMR reload is triggered.
47
+ *
48
+ * When the content differs (or the file is missing) it writes to a temp file in
49
+ * the same directory and renames it over the target. rename() is atomic on the
50
+ * same filesystem and swaps the inode, so watchers pick up a single clean
51
+ * replacement instead of observing a truncate-then-write mid-flight.
52
+ *
53
+ * Returns true if the file was written, false if it was unchanged and skipped.
54
+ */
55
+ async function writeAtomic(filePath, data, encoding = 'utf8') {
56
+ try {
57
+ const existing = await fs.readFile(filePath, encoding);
58
+ if (existing === data)
59
+ return false;
60
+ }
61
+ catch {
62
+ // File does not exist (or is unreadable) -> fall through and write it.
63
+ }
64
+ const tmpPath = `${filePath}.tmp`;
65
+ await fs.writeFile(tmpPath, data, encoding);
66
+ await fs.rename(tmpPath, filePath);
67
+ return true;
68
+ }
42
69
  /**
43
70
  * Import a module with alias resolution using esbuild.
44
71
  * Falls back to direct import when no aliases are configured.
@@ -93,6 +120,9 @@ export async function build(configPath = 'esm-styles.config.js') {
93
120
  // Ensure output directory exists
94
121
  await fs.mkdir(outputPath, { recursive: true });
95
122
  const cssFiles = [];
123
+ // Track whether any output file actually changed on disk. Used to decide
124
+ // whether the timestamp (an HMR trigger) needs to be bumped at the end.
125
+ let changed = false;
96
126
  // 2. Process globalVariables
97
127
  if (config.globalVariables) {
98
128
  const inputFile = path.join(sourcePath, `${config.globalVariables}${suffix}`);
@@ -103,7 +133,7 @@ export async function build(configPath = 'esm-styles.config.js') {
103
133
  const rootSelector = config.globalRootSelector || ':root';
104
134
  const comment = generateCssComment(config.globalVariables);
105
135
  const wrappedCss = `${comment}${rootSelector} {\n${cssVars}\n}`;
106
- await fs.writeFile(outputFile, wrappedCss, 'utf8');
136
+ changed = (await writeAtomic(outputFile, wrappedCss)) || changed;
107
137
  cssFiles.push({ type: 'global', file: 'global.css' });
108
138
  }
109
139
  // 3. Process media variable sets
@@ -141,7 +171,7 @@ export async function build(configPath = 'esm-styles.config.js') {
141
171
  }
142
172
  const comment = generateCssComment(setName);
143
173
  const block = `${comment}${fullSelector} {\n${cssVars}\n}`;
144
- await fs.writeFile(outputFile, block, 'utf8');
174
+ changed = (await writeAtomic(outputFile, block)) || changed;
145
175
  cssFiles.push({
146
176
  type: 'media',
147
177
  file: fileName,
@@ -226,7 +256,7 @@ export async function build(configPath = 'esm-styles.config.js') {
226
256
  const supportingModuleObj = buildSupportingModule([], true);
227
257
  const supportingModulePath = path.join(sourcePath, `$${mediaType}.mjs`);
228
258
  const moduleContent = `export default ${JSON.stringify(supportingModuleObj, null, 2)}\n`;
229
- await fs.writeFile(supportingModulePath, moduleContent, 'utf8');
259
+ changed = (await writeAtomic(supportingModulePath, moduleContent)) || changed;
230
260
  }
231
261
  }
232
262
  // 4. Process each floor (replaces legacy layers)
@@ -270,7 +300,7 @@ export async function build(configPath = 'esm-styles.config.js') {
270
300
  });
271
301
  wrappedCss = result.code;
272
302
  }
273
- await fs.writeFile(outputFile, wrappedCss, 'utf8');
303
+ changed = (await writeAtomic(outputFile, wrappedCss)) || changed;
274
304
  // Calculate relative path from default output directory for imports
275
305
  const relativePath = floorOutputPath
276
306
  ? path.relative(outputPath, outputFile)
@@ -319,17 +349,16 @@ export async function build(configPath = 'esm-styles.config.js') {
319
349
  .filter(Boolean)
320
350
  .join('\n') +
321
351
  '\n';
322
- await fs.writeFile(mainCssPath, mainCss, 'utf8');
323
- // 6. Create timestamp file
324
- const { outputPath: timestampOutputPath, extension: timestampExtension } = config.timestamp || { outputPath: '', extension: 'mjs' };
325
- const timestampPath = path.join(config.basePath || '.', timestampOutputPath, 'timestamp.' + timestampExtension);
326
- // Write atomically: write to a temp file in the same directory, then rename
327
- // over the target. rename() is atomic on the same filesystem and swaps the
328
- // inode, so file watchers (Vite, Cursor) reliably pick up the change instead
329
- // of observing a truncate-then-write mid-flight.
330
- const timestampTmpPath = `${timestampPath}.tmp`;
331
- await fs.writeFile(timestampTmpPath, `export default ${Date.now()}`, 'utf8');
332
- await fs.rename(timestampTmpPath, timestampPath);
352
+ changed = (await writeAtomic(mainCssPath, mainCss)) || changed;
353
+ // 6. Create timestamp file.
354
+ // The timestamp embeds Date.now() and is the HMR trigger, so only bump it
355
+ // when at least one output actually changed. A no-op rebuild leaves every
356
+ // file (including the timestamp) untouched and triggers no reload.
357
+ if (changed) {
358
+ const { outputPath: timestampOutputPath, extension: timestampExtension } = config.timestamp || { outputPath: '', extension: 'mjs' };
359
+ const timestampPath = path.join(config.basePath || '.', timestampOutputPath, 'timestamp.' + timestampExtension);
360
+ await writeAtomic(timestampPath, `export default ${Date.now()}`);
361
+ }
333
362
  }
334
363
  // Helper for file URL import
335
364
  import { pathToFileURL as pathToFileUrl } from 'node:url';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esm-styles",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "A library for working with ESM styles",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",