@codefast/cli 0.3.16-canary.2 → 0.3.16-canary.3

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.
package/README.md CHANGED
@@ -21,6 +21,10 @@ A small developer CLI for maintenance tasks in a TypeScript monorepo — Tailwin
21
21
  - [`mirror sync`](#mirror-sync)
22
22
  - [`tag` / `annotate`](#tag--annotate)
23
23
  - [Configuration (`codefast.config.*`)](#configuration-codefastconfig)
24
+ - [Full skeleton](#full-skeleton)
25
+ - [`mirror` configuration](#mirror-configuration)
26
+ - [`tag` configuration](#tag-configuration)
27
+ - [`arrange` configuration](#arrange-configuration)
24
28
  - [Lifecycle hooks](#lifecycle-hooks)
25
29
  - [Grouping philosophy — Render Pipeline Order](#grouping-philosophy--render-pipeline-order)
26
30
  - [Troubleshooting](#troubleshooting)
@@ -228,37 +232,46 @@ What it updates:
228
232
 
229
233
  ## Configuration (`codefast.config.*`)
230
234
 
231
- Create `codefast.config.js`, `.mjs`, `.cjs`, or `.json` at the repo root. Each command reads its own slice.
235
+ Create a config file at the **monorepo root** (next to `pnpm-workspace.yaml`). Supported names, in priority order:
236
+
237
+ | File name | Format |
238
+ | ---------------------- | --------------------------------------- |
239
+ | `codefast.config.mjs` | ES module — `export default { … }` |
240
+ | `codefast.config.js` | ES module (if `"type":"module"`) or CJS |
241
+ | `codefast.config.cjs` | CommonJS — `module.exports = { … }` |
242
+ | `codefast.config.json` | Plain JSON (no functions — no hooks) |
243
+
244
+ The file is found by walking up from the working directory, so running the CLI from any sub-directory still picks up the root config.
245
+
246
+ > **Security.** `.js`, `.mjs`, and `.cjs` files are executed via `import()`. Only run `codefast` inside repositories you trust.
247
+
248
+ ---
249
+
250
+ ### Full skeleton
232
251
 
233
252
  ```javascript
234
253
  // codefast.config.mjs
235
254
  import { execSync } from "node:child_process";
236
255
 
237
256
  export default {
257
+ // ─── mirror ────────────────────────────────────────────────────────────────
258
+ // Keys are package names (from package.json#name).
259
+ // Set a package to `false` to skip it entirely.
260
+ // Omit a package to process it with default settings.
238
261
  mirror: {
239
- skipPackages: ["@acme/internal"],
240
- pathTransformations: {
241
- "@acme/ui": { removePrefix: "./components/" },
242
- },
243
- customExports: {
244
- "@acme/ui": {
245
- "./css/*": "./src/styles/*",
246
- },
247
- },
248
- cssExports: {
249
- // Shorthand: `true` enables default CSS export detection
250
- "@acme/theme": true,
251
- // Or configure explicitly:
252
- "@acme/ui": {
253
- enabled: true,
254
- forceExportFiles: false,
255
- customExports: {
256
- "./tokens.css": "./dist/tokens.css",
257
- },
258
- },
262
+ "@acme/ui": {
263
+ strip: "./components/",
264
+ exports: { "./css/*": "./src/css/*" },
265
+ source: true, // default: true
266
+ types: true, // default: true
267
+ import: true, // default: true
268
+ css: true,
259
269
  },
270
+ "@acme/internal": false,
271
+ "@acme/docs": false,
260
272
  },
261
273
 
274
+ // ─── tag ───────────────────────────────────────────────────────────────────
262
275
  tag: {
263
276
  skipPackages: ["@acme/internal"],
264
277
  onAfterWrite: ({ files }) => {
@@ -266,6 +279,7 @@ export default {
266
279
  },
267
280
  },
268
281
 
282
+ // ─── arrange ───────────────────────────────────────────────────────────────
269
283
  arrange: {
270
284
  onAfterWrite: ({ files }) => {
271
285
  execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
@@ -274,31 +288,177 @@ export default {
274
288
  };
275
289
  ```
276
290
 
277
- Notes:
291
+ ---
292
+
293
+ ### `mirror` configuration
294
+
295
+ `mirror` is a record keyed by **package name** (the `name` field in the package's `package.json`, e.g. `"@acme/ui"`).
296
+
297
+ #### Skipping a package
298
+
299
+ Set a package to `false` to exclude it from `codefast mirror sync` entirely:
300
+
301
+ ```javascript
302
+ mirror: {
303
+ "@acme/internal": false,
304
+ "@acme/docs": false,
305
+ }
306
+ ```
307
+
308
+ Packages not mentioned in the config are processed with default settings.
309
+
310
+ #### Per-package options
311
+
312
+ Each package entry is an object with the following fields:
313
+
314
+ | Field | Type | Default | Description |
315
+ | ---------- | ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
316
+ | `source` | `boolean \| string` | `true` | Add a `source` condition to each export entry pointing to the original `.ts` file. `true` auto-derives the path (`./src/<module>.ts`). Pass a string to set the root-export path explicitly (`"./src/index.tsx"`). Set to `false` to omit. |
317
+ | `types` | `boolean` | `true` | Include the `types` condition when a `.d.ts` file is present. Set to `false` to omit. |
318
+ | `import` | `boolean` | `true` | Include the `import` condition. Set to `false` to omit (useful for CJS-only packages). |
319
+ | `strip` | `string` | — | Strip a leading path segment from generated export specifiers. See below. |
320
+ | `exports` | `Record<string, string>` | — | Add or override specific export specifiers. See below. |
321
+ | `preserve` | `boolean` | — | Keep the existing `package.json#exports` and only fill in missing conditions. |
322
+ | `css` | `boolean \| CssConfig` | — | Enable CSS export detection. See below. |
323
+
324
+ #### `strip`
325
+
326
+ Removes a fixed prefix from every generated export specifier. Use this when a package's `dist/` mirrors deep directory structure that you want to flatten in the public API.
327
+
328
+ ```javascript
329
+ // Without strip, dist/components/button.mjs → "./components/button"
330
+ // With strip: "./components/", it becomes → "./button"
331
+ "@acme/ui": {
332
+ strip: "./components/",
333
+ }
334
+ ```
335
+
336
+ The original file path is preserved for sorting — only the public specifier changes.
337
+
338
+ #### `exports`
278
339
 
279
- - Keys in `mirror.skipPackages` / `pathTransformations` / `customExports` / `cssExports` are **package names** from `package.json#name` (e.g. `@acme/ui`). Path-based keys such as `packages/ui` are deprecated and will be removed.
280
- - `cssExports[pkg]` accepts a boolean shorthand or the full `{ enabled, customExports, forceExportFiles }` object.
281
- - `tag.skipPackages` lists package names to skip entirely when `codefast tag` is run without an explicit target.
340
+ Adds or overrides specific specifiers in the final export map. Keys and values are the exact strings written into `package.json#exports`.
282
341
 
283
- > **Security.** `.js`, `.mjs`, and `.cjs` config files are loaded via `import()` — only run `codefast` inside repositories you trust.
342
+ ```javascript
343
+ "@acme/ui": {
344
+ exports: {
345
+ "./css/*": "./src/css/*", // wildcard passthrough to sources
346
+ "./tokens": "./dist/tokens.js", // explicit extra entry
347
+ },
348
+ }
349
+ ```
350
+
351
+ Extra entries are merged after auto-generation. They win over anything the scanner would produce for the same specifier. `./package.json` cannot be overridden.
352
+
353
+ #### `preserve`
354
+
355
+ Keeps the existing `package.json#exports` map exactly as-is and only fills in missing conditions (`source`, `types`, `import`) for each entry — no `dist/` scan is performed. Use this when you maintain the exports map by hand and only want the CLI to supplement missing conditions.
356
+
357
+ ```javascript
358
+ "@acme/tailwind-variants": {
359
+ preserve: true,
360
+ }
361
+ ```
362
+
363
+ #### `css`
364
+
365
+ Controls CSS file export generation. `mirror sync` scans `dist/` for `.css` files and writes wildcard or per-file export entries.
366
+
367
+ ```javascript
368
+ // Shorthand: auto-detect all CSS files in dist/
369
+ "@acme/theme": { css: true }
370
+
371
+ // Full config:
372
+ "@acme/ui": {
373
+ css: {
374
+ enabled: true,
375
+ // Force individual file entries instead of directory wildcards:
376
+ forceExportFiles: false,
377
+ // Manually add or override individual CSS specifiers:
378
+ customExports: {
379
+ "./tokens.css": "./dist/tokens.css",
380
+ },
381
+ },
382
+ }
383
+
384
+ // Explicitly disable CSS exports for this package:
385
+ "@acme/legacy": { css: false }
386
+ ```
387
+
388
+ When `css` is omitted, CSS files found in `dist/` are still exported by default.
389
+
390
+ #### What `mirror sync` writes
391
+
392
+ For a package with `dist/button.mjs`, `dist/button.d.ts`, and `source: true`, the generated export entry looks like:
393
+
394
+ ```json
395
+ {
396
+ "./button": {
397
+ "source": "./src/button.ts",
398
+ "types": "./dist/button.d.ts",
399
+ "import": "./dist/button.mjs"
400
+ }
401
+ }
402
+ ```
403
+
404
+ It also updates the top-level `main`, `module`, and `types` fields from the root (`.`) export, and ensures `"dist"` is listed in `files`.
405
+
406
+ ---
407
+
408
+ ### `tag` configuration
409
+
410
+ ```javascript
411
+ tag: {
412
+ // Package names to skip when running without an explicit target
413
+ skipPackages: ["@acme/internal", "@acme/docs"],
414
+
415
+ // Called after files are written — use it to format or lint-fix
416
+ onAfterWrite: ({ files }) => {
417
+ execSync(`prettier --write ${files.join(" ")}`, { stdio: "inherit" });
418
+ },
419
+ }
420
+ ```
421
+
422
+ | Field | Type | Description |
423
+ | -------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
424
+ | `skipPackages` | `string[]` | Package names to skip when `codefast tag` is run without an explicit target. Has no effect when a target path is provided directly. |
425
+ | `onAfterWrite` | `(ctx: { files: string[] }) => void \| Promise<void>` | Lifecycle hook — runs after files are written. |
426
+
427
+ ---
428
+
429
+ ### `arrange` configuration
430
+
431
+ ```javascript
432
+ arrange: {
433
+ // Called after files are written by `codefast arrange apply`
434
+ onAfterWrite: ({ files }) => {
435
+ execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
436
+ },
437
+ }
438
+ ```
439
+
440
+ | Field | Type | Description |
441
+ | -------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
442
+ | `onAfterWrite` | `(ctx: { files: string[] }) => void \| Promise<void>` | Lifecycle hook — runs after `arrange apply` writes files. Not called by `arrange preview`. |
284
443
 
285
444
  ---
286
445
 
287
446
  ## Lifecycle hooks
288
447
 
289
- Both `tag` and `arrange apply` expose an `onAfterWrite` hook so teams can plug in their own formatter, lint-fix step, or codemod without embedding one in the CLI core.
448
+ Both `tag` and `arrange apply` call `onAfterWrite` immediately after writing files to disk. The hook receives the list of written file paths and can run any synchronous or asynchronous work — formatters, linters, codegen, notifications.
290
449
 
291
450
  ```javascript
292
451
  export default {
293
452
  tag: {
294
453
  onAfterWrite: async ({ files }) => {
295
- console.log(`Formatting ${files.length} files…`);
296
- // sync or async work
454
+ // async is supported
455
+ await runFormatter(files);
297
456
  },
298
457
  },
299
458
  arrange: {
300
459
  onAfterWrite: ({ files }) => {
301
- /* … */
460
+ // sync is fine too
461
+ execSync(`oxfmt ${files.join(" ")}`, { stdio: "inherit" });
302
462
  },
303
463
  },
304
464
  };
@@ -306,10 +466,11 @@ export default {
306
466
 
307
467
  Contract:
308
468
 
309
- - `tag.onAfterWrite?.({ files })` runs after `codefast tag` writes files.
310
- - `arrange.onAfterWrite?.({ files })` runs after `codefast arrange apply` writes files.
311
- - Hooks may be synchronous or asynchronous (`void | Promise<void>`).
312
- - Hook failures are reported on stderr; both commands exit with code `1` when the hook rejects.
469
+ - `tag.onAfterWrite` fires after `codefast tag` writes JSDoc annotations.
470
+ - `arrange.onAfterWrite` fires after `codefast arrange apply` rewrites class strings.
471
+ - Hook is **not** called on `--dry-run` or `arrange preview`.
472
+ - Hooks may be synchronous or `async` (`void | Promise<void>`).
473
+ - If the hook throws or rejects, the command reports the error on stderr and exits with code `1`.
313
474
 
314
475
  ---
315
476
 
@@ -1,20 +1,41 @@
1
1
  import { z } from "zod";
2
2
  //#region src/core/config/schema.ts
3
3
  const afterWriteHookSchema = z.custom((value) => typeof value === "function", { message: "Expected a function" });
4
+ const mirrorCssConfigSchema = z.union([z.boolean(), z.object({
5
+ enabled: z.boolean().optional(),
6
+ customExports: z.record(z.string(), z.string()).optional(),
7
+ forceExportFiles: z.boolean().optional()
8
+ }).strict()]);
4
9
  /**
10
+ * Per-package mirror configuration. Setting a package to `false` skips it entirely.
11
+ *
12
+ * - `source` — include a `source` condition pointing to the original `.ts` file (default `true`).
13
+ * Pass a string to override the root-export source path explicitly.
14
+ * - `types` — include the `types` condition when `.d.ts` files exist (default `true`).
15
+ * - `import` — include the `import` condition (default `true`).
16
+ * - `css` — CSS export configuration (wildcard or per-file).
17
+ *
5
18
  * @since 0.3.16-canary.0
6
19
  */
7
- const mirrorConfigSchema = z.object({
8
- skipPackages: z.array(z.string()).optional(),
9
- pathTransformations: z.record(z.string(), z.object({ removePrefix: z.string().optional() }).strict()).optional(),
10
- customExports: z.record(z.string(), z.record(z.string(), z.string())).optional(),
11
- cssExports: z.record(z.string(), z.union([z.boolean(), z.object({
12
- enabled: z.boolean().optional(),
13
- customExports: z.record(z.string(), z.string()).optional(),
14
- forceExportFiles: z.boolean().optional()
15
- }).strict()])).optional()
20
+ const mirrorPackageConfigSchema = z.object({
21
+ /** Preserve the existing `package.json#exports` map and only add missing conditions
22
+ * (`source`, `types`, `import`). No dist/ scan is performed. */
23
+ preserve: z.boolean().optional(),
24
+ strip: z.string().optional(),
25
+ exports: z.record(z.string(), z.string()).optional(),
26
+ source: z.union([z.boolean(), z.string()]).default(true),
27
+ types: z.boolean().default(true),
28
+ import: z.boolean().default(true),
29
+ css: mirrorCssConfigSchema.optional()
16
30
  }).strict();
17
31
  /**
32
+ * Mirror config: a record keyed by package name. Set a package to `false` to skip it;
33
+ * omit it entirely to process it with default settings.
34
+ *
35
+ * @since 0.3.16-canary.0
36
+ */
37
+ const mirrorConfigSchema = z.record(z.string(), z.union([z.literal(false), mirrorPackageConfigSchema]));
38
+ /**
18
39
  * @since 0.3.16-canary.0
19
40
  */
20
41
  const codefastTagConfigSchema = z.object({
@@ -36,4 +57,4 @@ const codefastConfigRootSchema = z.object({
36
57
  arrange: codefastArrangeConfigSchema.optional()
37
58
  }).strict();
38
59
  //#endregion
39
- export { codefastArrangeConfigSchema, codefastConfigRootSchema, codefastTagConfigSchema, mirrorConfigSchema };
60
+ export { codefastArrangeConfigSchema, codefastConfigRootSchema, codefastTagConfigSchema, mirrorConfigSchema, mirrorPackageConfigSchema };
@@ -1,10 +1,6 @@
1
1
  import { PACKAGE_JSON_EXPORT, VALID_DTS_EXTENSIONS, VALID_JS_EXTENSIONS } from "./constants.mjs";
2
2
  import * as nodePath from "node:path";
3
3
  //#region src/mirror/domain/exports.ts
4
- function resolvePackageScopedConfig(configMap, pkgMeta) {
5
- if (!configMap) return;
6
- return configMap[pkgMeta.packageName];
7
- }
8
4
  function groupDistFilesByModule(relativeDistFiles) {
9
5
  const distModulesByPath = /* @__PURE__ */ new Map();
10
6
  for (const relativeDistFile of relativeDistFiles) {
@@ -110,14 +106,11 @@ function getExportSortGroup(exportPath, pathTransform) {
110
106
  /**
111
107
  * @since 0.3.16-canary.0
112
108
  */
113
- function createPathTransform(config, pkgMeta) {
114
- const pathConfig = resolvePackageScopedConfig(config?.pathTransformations, pkgMeta);
115
- if (!pathConfig) return null;
116
- const { removePrefix } = pathConfig;
117
- if (!removePrefix) return null;
109
+ function createPathTransform(strip) {
110
+ if (!strip) return null;
118
111
  return (exportPath) => {
119
- if (!exportPath.startsWith(removePrefix)) return exportPath;
120
- const trimmedExportPath = exportPath.slice(removePrefix.length);
112
+ if (!exportPath.startsWith(strip)) return exportPath;
113
+ const trimmedExportPath = exportPath.slice(strip.length);
121
114
  if (trimmedExportPath && trimmedExportPath !== "." && !trimmedExportPath.startsWith("./")) return `./${trimmedExportPath}`;
122
115
  return trimmedExportPath;
123
116
  };
@@ -174,7 +167,7 @@ async function generateCssExports(fileSystemService, distDir, cssConfig) {
174
167
  *
175
168
  * @since 0.3.16-canary.0
176
169
  */
177
- async function generateExports(fileSystemService, distDir, pathTransform, cssConfig, customExports) {
170
+ async function generateExports(fileSystemService, distDir, pathTransform, cssConfig, extraExports, options) {
178
171
  const relativeDistFiles = await fileSystemService.listRelativeFilesRecursively(distDir);
179
172
  if (!relativeDistFiles.length) return {
180
173
  exports: { [PACKAGE_JSON_EXPORT]: PACKAGE_JSON_EXPORT },
@@ -197,10 +190,14 @@ async function generateExports(fileSystemService, distDir, pathTransform, cssCon
197
190
  let exportPath = originalExportPath;
198
191
  if (pathTransform) exportPath = pathTransform(exportPath);
199
192
  const exportEntry = {};
193
+ if (options?.source) if (typeof options.source === "string" && exportPath === ".") exportEntry.source = options.source;
194
+ else exportEntry.source = (options.resolveSourcePath ?? ((p) => `./src/${p}.ts`))(distModuleEntry.path);
200
195
  const declarationFile = distModuleEntry.files.dts;
201
- if (declarationFile) exportEntry.types = `./dist/${declarationFile}`;
202
- if (distModuleEntry.files.mjs) exportEntry.import = `./dist/${distModuleEntry.files.mjs}`;
203
- else if (distModuleEntry.files.js) exportEntry.import = `./dist/${distModuleEntry.files.js}`;
196
+ if (declarationFile && options?.types !== false) exportEntry.types = `./dist/${declarationFile}`;
197
+ if (options?.import !== false) {
198
+ if (distModuleEntry.files.mjs) exportEntry.import = `./dist/${distModuleEntry.files.mjs}`;
199
+ else if (distModuleEntry.files.js) exportEntry.import = `./dist/${distModuleEntry.files.js}`;
200
+ }
204
201
  if (distModuleEntry.files.cjs) exportEntry.require = `./dist/${distModuleEntry.files.cjs}`;
205
202
  moduleExportsBySpecifier[exportPath] = exportEntry;
206
203
  originalPathBySpecifier[exportPath] = originalExportPath;
@@ -210,9 +207,9 @@ async function generateExports(fileSystemService, distDir, pathTransform, cssCon
210
207
  for (const exportKey of sortedSpecifiers) sortedExports[exportKey] = moduleExportsBySpecifier[exportKey];
211
208
  const cssExports = await generateCssExports(fileSystemService, distDir, cssConfig ?? { enabled: true });
212
209
  Object.assign(sortedExports, cssExports);
213
- for (const [specifier, mappedPath] of Object.entries(customExports || {})) if (specifier !== "./package.json") sortedExports[specifier] = mappedPath;
210
+ for (const [specifier, mappedPath] of Object.entries(extraExports || {})) if (specifier !== "./package.json") sortedExports[specifier] = mappedPath;
214
211
  for (const cssSpecifier of Object.keys(cssExports)) if (!(cssSpecifier in originalPathBySpecifier)) originalPathBySpecifier[cssSpecifier] = cssSpecifier;
215
- for (const customSpecifier of Object.keys(customExports || {})) if (customSpecifier !== "./package.json" && !(customSpecifier in originalPathBySpecifier)) originalPathBySpecifier[customSpecifier] = customSpecifier;
212
+ for (const extraSpecifier of Object.keys(extraExports || {})) if (extraSpecifier !== "./package.json" && !(extraSpecifier in originalPathBySpecifier)) originalPathBySpecifier[extraSpecifier] = extraSpecifier;
216
213
  sortedSpecifiers = Object.keys(sortedExports).filter((exportKey) => exportKey !== PACKAGE_JSON_EXPORT).sort((leftSpecifier, rightSpecifier) => compareExportSortKeys(getExportSortKey(leftSpecifier, pathTransform), getExportSortKey(rightSpecifier, pathTransform)));
217
214
  const finalExports = {};
218
215
  for (const exportKey of sortedSpecifiers) finalExports[exportKey] = sortedExports[exportKey];
@@ -0,0 +1,114 @@
1
+ import { DIST_DIR } from "./domain/constants.mjs";
2
+ import { writePackageJsonExportsAtomic } from "./write-exports.mjs";
3
+ import path from "node:path";
4
+ //#region src/mirror/supplement-exports.ts
5
+ const DTS_EXTENSIONS = [
6
+ ".d.mts",
7
+ ".d.ts",
8
+ ".d.cts"
9
+ ];
10
+ const JS_EXTENSIONS = [".mjs", ".js"];
11
+ /**
12
+ * Infer the dist module path (e.g. `"index"`, `"components/button"`) from an existing
13
+ * conditional export entry. Prefers `import` / `require` values because their extensions
14
+ * are simpler than `.d.mts` etc.
15
+ */
16
+ function inferModulePath(specifier, entry) {
17
+ for (const key of ["import", "require"]) {
18
+ const value = entry[key];
19
+ if (typeof value === "string" && value.startsWith("./dist/")) {
20
+ const relative = value.slice(7);
21
+ for (const ext of JS_EXTENSIONS) if (relative.endsWith(ext)) return relative.slice(0, -ext.length);
22
+ }
23
+ }
24
+ const typesValue = entry.types;
25
+ if (typeof typesValue === "string" && typesValue.startsWith("./dist/")) {
26
+ const relative = typesValue.slice(7);
27
+ for (const ext of DTS_EXTENSIONS) if (relative.endsWith(ext)) return relative.slice(0, -ext.length);
28
+ }
29
+ if (specifier === ".") return "index";
30
+ if (specifier.startsWith("./")) return specifier.slice(2);
31
+ return specifier;
32
+ }
33
+ function findDtsSpecifier(fs, distDir, modulePath) {
34
+ for (const ext of DTS_EXTENSIONS) if (fs.existsSync(path.join(distDir, `${modulePath}${ext}`))) return `./dist/${modulePath}${ext}`;
35
+ return null;
36
+ }
37
+ function findImportSpecifier(fs, distDir, modulePath) {
38
+ for (const ext of JS_EXTENSIONS) if (fs.existsSync(path.join(distDir, `${modulePath}${ext}`))) return `./dist/${modulePath}${ext}`;
39
+ return null;
40
+ }
41
+ /**
42
+ * Rebuild an export entry in canonical key order, adding any missing conditions.
43
+ * Key order: source → types → import → require → (remaining original keys).
44
+ */
45
+ function buildSupplementedEntry(specifier, existing, modulePath, distDir, fs, options) {
46
+ const result = {};
47
+ const knownKeys = new Set([
48
+ "source",
49
+ "types",
50
+ "import",
51
+ "require"
52
+ ]);
53
+ if ("source" in existing) result.source = existing.source;
54
+ else if (options.source) if (typeof options.source === "string" && specifier === ".") result.source = options.source;
55
+ else result.source = options.resolveSourcePath(modulePath);
56
+ if ("types" in existing) result.types = existing.types;
57
+ else if (options.types) {
58
+ const found = findDtsSpecifier(fs, distDir, modulePath);
59
+ if (found) result.types = found;
60
+ }
61
+ if ("import" in existing) result.import = existing.import;
62
+ else if (options.import) {
63
+ const found = findImportSpecifier(fs, distDir, modulePath);
64
+ if (found) result.import = found;
65
+ }
66
+ if ("require" in existing) result.require = existing.require;
67
+ for (const [key, value] of Object.entries(existing)) if (!knownKeys.has(key) && !(key in result)) result[key] = value;
68
+ return result;
69
+ }
70
+ /**
71
+ * @since 0.3.16-canary.0
72
+ */
73
+ async function supplementExportsInPackageJson(fs, packageJsonPath, packageDir, options) {
74
+ const raw = await fs.readFile(packageJsonPath, "utf8");
75
+ const existingExports = JSON.parse(raw).exports;
76
+ if (!existingExports || typeof existingExports !== "object" || Array.isArray(existingExports)) return { supplementedSpecifiers: [] };
77
+ const distDir = path.join(packageDir, DIST_DIR);
78
+ const supplementedSpecifiers = [];
79
+ const supplementedExports = {};
80
+ const originalPathBySpecifier = {};
81
+ for (const [specifier, entry] of Object.entries(existingExports)) {
82
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
83
+ supplementedExports[specifier] = entry;
84
+ originalPathBySpecifier[specifier] = specifier;
85
+ continue;
86
+ }
87
+ const entryRecord = entry;
88
+ const supplemented = buildSupplementedEntry(specifier, entryRecord, inferModulePath(specifier, entryRecord), distDir, fs, options);
89
+ if (JSON.stringify(supplemented) !== JSON.stringify(entryRecord)) supplementedSpecifiers.push(specifier);
90
+ supplementedExports[specifier] = supplemented;
91
+ originalPathBySpecifier[specifier] = specifier;
92
+ }
93
+ await writePackageJsonExportsAtomic(fs, packageJsonPath, {
94
+ generatedExports: supplementedExports,
95
+ managedExportSpecifiers: Object.keys(supplementedExports),
96
+ originalPathBySpecifier
97
+ });
98
+ return { supplementedSpecifiers };
99
+ }
100
+ /**
101
+ * Build a `resolveSourcePath` closure that checks the filesystem for `.tsx` before
102
+ * falling back to `.ts`. Mirrors the logic used in normal (non-custom) mode.
103
+ *
104
+ * @since 0.3.16-canary.0
105
+ */
106
+ function buildSourcePathResolver(fs, packageDir) {
107
+ const srcDir = path.join(packageDir, "src");
108
+ return (modulePath) => {
109
+ const tsxPath = path.join(srcDir, `${modulePath}.tsx`);
110
+ return fs.existsSync(tsxPath) ? `./src/${modulePath}.tsx` : `./src/${modulePath}.ts`;
111
+ };
112
+ }
113
+ //#endregion
114
+ export { buildSourcePathResolver, supplementExportsInPackageJson };
@@ -65,7 +65,7 @@ var MirrorSyncReporter = class {
65
65
  const breakdown = [];
66
66
  if (generatedDistAssetCounts.jsCount > 0) breakdown.push(this.paint(`${generatedDistAssetCounts.jsCount} modules`, ANSI.green));
67
67
  if (generatedDistAssetCounts.cssCount > 0) breakdown.push(this.paint(`${generatedDistAssetCounts.cssCount} CSS`, ANSI.magenta));
68
- if (pkgStats.customExports > 0) breakdown.push(this.paint(`${pkgStats.customExports} custom`, ANSI.yellow));
68
+ if (pkgStats.extraExports > 0) breakdown.push(this.paint(`${pkgStats.extraExports} custom`, ANSI.yellow));
69
69
  const totalExportsText = this.paint(`${pkgStats.totalExports} exports`, ANSI.brightCyan);
70
70
  if (breakdown.length === 0) logger.out(` ${this.paint("└─", ANSI.dim)} ${totalExportsText}`);
71
71
  else logger.out(` ${this.paint("└─", ANSI.dim)} ${breakdown.join(" + ")} = ${totalExportsText}`);
@@ -4,6 +4,7 @@ import { createPathTransform, generateExports } from "./domain/exports.mjs";
4
4
  import { resolvePackageDisplayName } from "./domain/package-display-name.mjs";
5
5
  import { writePackageJsonExportsAtomic } from "./write-exports.mjs";
6
6
  import { createMirrorDistFilesystem } from "./dist-filesystem-impl.mjs";
7
+ import { buildSourcePathResolver, supplementExportsInPackageJson } from "./supplement-exports.mjs";
7
8
  import path from "node:path";
8
9
  //#region src/mirror/sync-workspace-package.ts
9
10
  /**
@@ -23,7 +24,7 @@ async function syncExportsForWorkspacePackage(fs, rootDir, packagePathStr, confi
23
24
  path: packageDir,
24
25
  jsModules: 0,
25
26
  cssExports: 0,
26
- customExports: 0,
27
+ extraExports: 0,
27
28
  totalExports: 0,
28
29
  hasTransform: false,
29
30
  cssConfigStatus: "",
@@ -49,8 +50,8 @@ async function syncExportsForWorkspacePackage(fs, rootDir, packagePathStr, confi
49
50
  pkgStats.name = folderBasename;
50
51
  packageJsonParseError = caughtError;
51
52
  }
52
- const packageMeta = { packageName: pkgStats.name };
53
- if (isPackageSkipped(config.skipPackages, packageMeta)) {
53
+ const pkgConfig = config[pkgStats.name];
54
+ if (pkgConfig === false) {
54
55
  pkgStats.skipped = true;
55
56
  pkgStats.skipReason = "configured to skip";
56
57
  return pkgStats;
@@ -70,34 +71,39 @@ async function syncExportsForWorkspacePackage(fs, rootDir, packagePathStr, confi
70
71
  return pkgStats;
71
72
  }
72
73
  try {
73
- const pathTransform = createPathTransform(config, packageMeta);
74
- pkgStats.hasTransform = !!pathTransform;
75
- const cssConfig = resolvePackageScopedConfig(config.cssExports, packageMeta);
76
- if (cssConfig === false) pkgStats.cssConfigStatus = "disabled";
77
- else if (cssConfig !== void 0) pkgStats.cssConfigStatus = "configured";
78
- const customExports = resolvePackageScopedConfig(config.customExports, packageMeta) || {};
79
- const generatedExports = await generateExports(distFilesystem, distDir, pathTransform, cssConfig, customExports);
80
- const { prunedKeys } = await writePackageJsonExportsAtomic(fs, packageJsonPath, {
81
- generatedExports: generatedExports.exports,
82
- managedExportSpecifiers: Object.keys(generatedExports.exports),
83
- originalPathBySpecifier: generatedExports.originalPathBySpecifier
84
- });
85
- pkgStats.jsModules = generatedExports.jsCount;
86
- pkgStats.cssExports = generatedExports.cssCount;
87
- pkgStats.customExports = Object.keys(customExports).length;
88
- pkgStats.totalExports = Object.keys(generatedExports.exports).length;
89
- pkgStats.prunedExportKeys = prunedKeys;
74
+ const resolveSourcePath = buildSourcePathResolver(fs, packageDir);
75
+ const exportOptions = {
76
+ source: pkgConfig?.source ?? true,
77
+ types: pkgConfig?.types ?? true,
78
+ import: pkgConfig?.import ?? true,
79
+ resolveSourcePath
80
+ };
81
+ if (pkgConfig?.preserve) {
82
+ const { supplementedSpecifiers } = await supplementExportsInPackageJson(fs, packageJsonPath, packageDir, exportOptions);
83
+ pkgStats.totalExports = supplementedSpecifiers.length;
84
+ } else {
85
+ const pathTransform = createPathTransform(pkgConfig?.strip);
86
+ pkgStats.hasTransform = !!pathTransform;
87
+ const cssConfig = pkgConfig?.css;
88
+ if (cssConfig === false) pkgStats.cssConfigStatus = "disabled";
89
+ else if (cssConfig !== void 0) pkgStats.cssConfigStatus = "configured";
90
+ const extraExports = pkgConfig?.exports ?? {};
91
+ const generatedExports = await generateExports(distFilesystem, distDir, pathTransform, cssConfig, extraExports, exportOptions);
92
+ const { prunedKeys } = await writePackageJsonExportsAtomic(fs, packageJsonPath, {
93
+ generatedExports: generatedExports.exports,
94
+ managedExportSpecifiers: Object.keys(generatedExports.exports),
95
+ originalPathBySpecifier: generatedExports.originalPathBySpecifier
96
+ });
97
+ pkgStats.jsModules = generatedExports.jsCount;
98
+ pkgStats.cssExports = generatedExports.cssCount;
99
+ pkgStats.extraExports = Object.keys(extraExports).length;
100
+ pkgStats.totalExports = Object.keys(generatedExports.exports).length;
101
+ pkgStats.prunedExportKeys = prunedKeys;
102
+ }
90
103
  } catch (caughtError) {
91
104
  pkgStats.error = messageFrom(caughtError);
92
105
  }
93
106
  return pkgStats;
94
107
  }
95
- function resolvePackageScopedConfig(configMap, packageMeta) {
96
- if (!configMap) return;
97
- return configMap[packageMeta.packageName];
98
- }
99
- function isPackageSkipped(skipPackagesList, packageMeta) {
100
- return !!skipPackagesList?.includes(packageMeta.packageName);
101
- }
102
108
  //#endregion
103
109
  export { syncExportsForWorkspacePackage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codefast/cli",
3
- "version": "0.3.16-canary.2",
3
+ "version": "0.3.16-canary.3",
4
4
  "description": "Developer CLI for the Codefast monorepo (arrange, mirror, tag)",
5
5
  "keywords": [
6
6
  "cli",
@@ -59,17 +59,17 @@
59
59
  "zod": "^4.4.3"
60
60
  },
61
61
  "devDependencies": {
62
- "@types/node": "^25.7.0",
62
+ "@types/node": "^25.9.1",
63
63
  "@types/picomatch": "^4.0.3",
64
- "@typescript/native-preview": "7.0.0-dev.20260514.1",
65
- "@vitest/coverage-v8": "^4.1.6",
64
+ "@typescript/native-preview": "7.0.0-dev.20260526.1",
65
+ "@vitest/coverage-v8": "^4.1.7",
66
66
  "tsdown": "^0.22.0",
67
- "vite": "^8.0.13",
68
- "vitest": "^4.1.6",
69
- "@codefast/typescript-config": "0.3.16-canary.2"
67
+ "vite": "^8.0.14",
68
+ "vitest": "^4.1.7",
69
+ "@codefast/typescript-config": "0.3.16-canary.3"
70
70
  },
71
71
  "engines": {
72
- "node": ">=22.0.0"
72
+ "node": ">=24.0.0"
73
73
  },
74
74
  "scripts": {
75
75
  "build": "tsdown",