@dbx-tools/projen 0.9.111 → 0.9.116

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
@@ -381,16 +381,23 @@ added to `ignorePatterns` at synth - named individually via
381
381
  package may hold hand-written modules beside its generated ones, and those must
382
382
  stay linted.
383
383
 
384
- `generateBarrels()` writes package-root `index.ts` barrels with module
385
- namespaces, flat unique type exports, and `PACKAGE_IDENTIFIER`, returning the
386
- number that actually changed. A name two modules both declare is ambiguous and stays namespace-only,
387
- except when one of them is generated: the hand-written module is the curated view
388
- of the generated shape (`shared-genie`'s `genie-model.ts` extends its own
389
- codegen'd `dashboards.ts`), so it owns the name and stays hoisted. A barrel whose export surface is unchanged is left untouched,
390
- read-only bit included, so concurrent writers never collide over it. Every
391
- package is attempted even if one fails; the failures are re-thrown together as an
392
- `AggregateError` naming each package, rather than the first one abandoning the
393
- rest of the sweep.
384
+ `generateBarrels()` writes the package-root `index.ts` and, for packages with the
385
+ generated `"./*": "./src/*/index.ts"` export, read-only entrypoints in nested
386
+ source directories. For example, `src/example/one.ts` becomes importable through
387
+ `@scope/package/example`, and `src/example/wasm/one.ts` through
388
+ `@scope/package/example/wasm`. A sibling `exports.ts` can extend or override the
389
+ generated surface at the package root or any nested level. Existing hand-written
390
+ `index.ts` entrypoints remain authoritative.
391
+
392
+ Generated barrels include module namespaces and flat unique type/value exports;
393
+ the package root additionally exposes `PACKAGE_IDENTIFIER`. A name two modules
394
+ both declare is ambiguous and stays namespace-only, except when one is generated:
395
+ the hand-written module is the curated view of the generated shape
396
+ (`shared-genie`'s `genie-model.ts` extends its own codegen'd `dashboards.ts`), so
397
+ it owns the name and stays hoisted. An unchanged barrel remains untouched,
398
+ including its read-only bit, so concurrent writers do not collide. Every package
399
+ is attempted even if one fails; failures are re-thrown together as an
400
+ `AggregateError` naming each package.
394
401
 
395
402
  ## Configure pnpm Catalogs
396
403
 
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
6
- export const PACKAGE_VERSION = "0.9.111";
6
+ export const PACKAGE_VERSION = "0.9.116";
7
7
  export * as barrels from "./src/barrels.ts";
8
8
  export * as bunApp from "./src/bun-app.ts";
9
9
  export * as bunWorkflow from "./src/bun-workflow.ts";
package/package.json CHANGED
@@ -30,10 +30,10 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@clack/prompts": "^1.7.0",
33
- "@dbx-tools/cli-args": "^0.9.111",
34
- "@dbx-tools/core": "^0.9.111",
35
- "@dbx-tools/path": "^0.9.111",
36
- "@dbx-tools/shared-core": "^0.9.111",
33
+ "@dbx-tools/cli-args": "^0.9.116",
34
+ "@dbx-tools/core": "^0.9.116",
35
+ "@dbx-tools/path": "^0.9.116",
36
+ "@dbx-tools/shared-core": "^0.9.116",
37
37
  "commander": "^15.0.0",
38
38
  "concurrently": "^10.0.3",
39
39
  "constructs": "^10.6.0",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "main": "index.ts",
57
57
  "license": "Apache-2.0",
58
- "version": "0.9.111",
58
+ "version": "0.9.116",
59
59
  "types": "index.ts",
60
60
  "type": "module",
61
61
  "exports": {
package/src/barrels.ts CHANGED
@@ -1,25 +1,28 @@
1
1
  /**
2
2
  * Barrel generator.
3
3
  *
4
- * For every package it writes a single `index.ts` **at the package root** (above
5
- * `src/`) that namespace-re-exports every module under `src/`, subject to a few
6
- * rules (see {@link isExcluded}):
4
+ * For every package it writes the package-root `index.ts` plus generated
5
+ * `src/**​/index.ts` subpath barrels when the package enables the generated
6
+ * nested export pattern. The root keeps its existing direct module
7
+ * namespaces; nested barrels expose each directory as
8
+ * `@scope/package/<directory>`. Both follow the same rules (see
9
+ * {@link isExcluded}):
7
10
  * 1. a file/folder whose name starts with `_` is private and never barrelled;
8
11
  * 2. test / `.d.ts` files are skipped;
9
12
  * 3. a hand-authored `src/**​/index.ts` is a subpath entry, while a generated
10
13
  * nested index is the facade for its generated folder;
11
14
  * 4. only files that actually contain an `export` are re-exported.
12
15
  *
13
- * A hand-authored `exports.ts` sitting next to the generated `index.ts` (a Vite-style
14
- * override) is spliced in last and wins: its exports are appended, and any generated
15
- * `export * as <ns>` whose namespace it also declares is dropped so the custom one
16
- * takes priority. This keeps the barrel auto-generated while letting you add or
17
- * override individual exports.
16
+ * A hand-authored `exports.ts` sitting next to any generated `index.ts` (a
17
+ * Vite-style override) is spliced in last and wins: its exports are appended,
18
+ * and any generated `export * as <ns>` whose namespace it also declares is
19
+ * dropped so the custom one takes priority. This keeps barrels auto-generated
20
+ * while letting you add or override individual exports at every depth.
18
21
  *
19
22
  *
20
- * Each eligible module becomes `export * as <name> from "./src/x.ts"` (camelCase
21
- * namespace from its path segments; invalid identifiers suffixed with `Module`),
22
- * sorted by module path.
23
+ * Each eligible module becomes `export * as <name>` from its barrel-relative
24
+ * path (camelCase namespace from its path segments; invalid identifiers
25
+ * suffixed with `Module`), sorted by module path.
23
26
  *
24
27
  * On top of the namespace lines, every export that is UNIQUE across the package
25
28
  * (declared in exactly one module) is also HOISTED to the barrel's top level, so
@@ -29,8 +32,8 @@
29
32
  * `export { ... }`. The module namespaces stay either way, so a namespaced call
30
33
  * site keeps working.
31
34
  *
32
- * Every generated barrel also exports `PACKAGE_IDENTIFIER` and `PACKAGE_VERSION`
33
- * from the package's own `package.json`.
35
+ * The package-root barrel also exports `PACKAGE_IDENTIFIER` and
36
+ * `PACKAGE_VERSION` from the package's own `package.json`.
34
37
  *
35
38
  * Uniqueness is tallied over types and values TOGETHER: a name carried by two
36
39
  * modules is ambiguous whichever kind it is, and hoisting one module's value
@@ -50,7 +53,7 @@
50
53
  * The result gets a do-not-edit header + read-only bit (see `./generated`).
51
54
  */
52
55
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
53
- import { join, relative } from "node:path";
56
+ import { dirname, join, relative } from "node:path";
54
57
  import { find } from "@dbx-tools/path";
55
58
  import { json, stringUtils } from "@dbx-tools/shared-core";
56
59
  import isIdentifier from "is-identifier";
@@ -133,9 +136,12 @@ function moduleSegmentToCamel(segment: string): string {
133
136
  return head + rest.join("");
134
137
  }
135
138
 
136
- /** Derive a valid namespace identifier from a relocated barrel module path. */
139
+ /** Derive a valid namespace identifier from a barrel-relative module path. */
137
140
  function modulePathToNamespace(modulePath: string): string {
138
- const rel = modulePath.replace(/^\.\/src\//, "").replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
141
+ const rel = modulePath
142
+ .replace(/^\.\//, "")
143
+ .replace(/^src\//, "")
144
+ .replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
139
145
  const segments = rel.split("/");
140
146
  if (segments.at(-1) === "index" && segments.length > 1) segments.pop();
141
147
  const names = segments.map(moduleSegmentToCamel);
@@ -153,11 +159,11 @@ function modulePathToNamespace(modulePath: string): string {
153
159
  return name;
154
160
  }
155
161
 
156
- /** A `./src/x` module path parsed out of a generated `export * as <ns>` line. */
162
+ /** A relative module path parsed out of a generated `export * as <ns>` line. */
157
163
  function namespaceLines(content: string): { ns: string; modulePath: string }[] {
158
164
  const out: { ns: string; modulePath: string }[] = [];
159
165
  for (const line of content.split("\n")) {
160
- const match = /^export \* as (\w+) from "(\.\/src\/.+)";\s*$/.exec(line);
166
+ const match = /^export \* as (\w+) from "(\.\/.+)";\s*$/.exec(line);
161
167
  if (match) out.push({ ns: match[1]!, modulePath: match[2]! });
162
168
  }
163
169
  return out;
@@ -174,7 +180,7 @@ function namespaceLines(content: string): { ns: string; modulePath: string }[] {
174
180
  * hand-authored `exports.ts` surface) are never hoisted so that file stays
175
181
  * authoritative.
176
182
  */
177
- function hoistUniqueExports(content: string, pkgDir: string, suppress: Set<string>): string {
183
+ function hoistUniqueExports(content: string, barrelDir: string, suppress: Set<string>): string {
178
184
  const namespaces = namespaceLines(content);
179
185
  if (namespaces.length === 0) return content;
180
186
 
@@ -197,7 +203,7 @@ function hoistUniqueExports(content: string, pkgDir: string, suppress: Set<strin
197
203
  const seen = new Map<string, { count: number; modulePath: string; generated: boolean }>();
198
204
  const perModule = new Map<string, ModuleExport[]>();
199
205
  for (const { modulePath } of namespaces) {
200
- const file = join(pkgDir, modulePath.replace(/^\.\//, ""));
206
+ const file = join(barrelDir, modulePath.replace(/^\.\//, ""));
201
207
  const exports = moduleExports(file).filter((e) => !e.isFunction);
202
208
  perModule.set(modulePath, exports);
203
209
  const generated = isGenerated(file);
@@ -271,8 +277,8 @@ function customExportNames(file: string): Set<string> {
271
277
  * custom export wins - a plain `export *` cannot otherwise override an explicit
272
278
  * `export * as`), then the whole module is re-exported last.
273
279
  */
274
- function mergeCustomExports(content: string, pkgDir: string): string {
275
- const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
280
+ function mergeCustomExports(content: string, barrelDir: string): string {
281
+ const customPath = join(barrelDir, CUSTOM_EXPORTS_FILE);
276
282
  if (!existsSync(customPath)) return content;
277
283
  const overridden = customExportNames(customPath);
278
284
  const kept = content.split("\n").filter((line) => {
@@ -324,108 +330,155 @@ function withPackageMetadata(content: string, pkgDir: string): string {
324
330
  }
325
331
 
326
332
  /**
327
- * Rebuild one package's root barrel. Returns 1 only if the barrel's contents
328
- * actually changed - a module was added, removed, renamed, or toggled its
329
- * `export` - and 0 for a no-op. An edit *inside* an already-exported module (even
330
- * adding a new named export) leaves the namespace `export * as … from "./src/x"`
331
- * list identical, so it is a no-op.
333
+ * True when a generated index belongs to this barrel generator rather than a
334
+ * binding/codegen owner that also uses the repository's generated-file header.
332
335
  */
333
- function generateForPackage(pkgDir: string): number {
334
- const srcDir = join(pkgDir, "src");
335
- if (!existsSync(srcDir)) return 0;
336
+ export function isGeneratedBarrel(file: string): boolean {
337
+ return existsSync(file) && readFileSync(file, "utf8").startsWith(header(BARREL_HEADER));
338
+ }
336
339
 
337
- const rootBarrel = join(pkgDir, "index.ts");
338
- // Snapshot the current barrel so we can tell a real change (module added/removed/
339
- // renamed) from an edit *inside* an already-exported module, which leaves the
340
- // `export * as … from "./src/x"` list - and therefore this file - byte-for-byte
341
- // identical.
342
- const before = existsSync(rootBarrel) ? readFileSync(rootBarrel, "utf8") : undefined;
343
-
344
- // The re-exportable module set under `src/`: every source file that actually
345
- // exports something, minus private / test / declaration files and hand-authored
346
- // `src/**/index.ts` subpath entries. A generated nested index is retained as
347
- // that generated folder's public facade. `findFiles`
348
- // yields posix paths relative to `srcDir`; `hasExport` parses each via
349
- // `moduleStatements` and needs the absolute path.
350
- const candidates = [...find.findFiles("**/*", { cwd: srcDir })]
340
+ /** Public direct modules and child-directory facades for one generated barrel. */
341
+ function barrelCandidates(barrelDir: string): string[] {
342
+ const files = [...find.findFiles("*", { cwd: barrelDir })]
351
343
  .map(toPosix)
352
- .filter(
353
- (file) =>
354
- isModuleFile(file) || (/(^|\/)index\.ts$/.test(file) && isGenerated(join(srcDir, file))),
355
- )
356
- .filter((f) => !isExcluded(f, srcDir))
357
- .filter((f) => hasExport(join(srcDir, f)));
344
+ .filter((file) => !file.includes("/"))
345
+ .filter((file) => file !== "index.ts" && file !== CUSTOM_EXPORTS_FILE)
346
+ .filter((file) => isModuleFile(file))
347
+ .filter((file) => !isExcluded(file, barrelDir))
348
+ .filter((file) => hasExport(join(barrelDir, file)));
349
+ const childIndexes = [...find.findFiles("*/index.ts", { cwd: barrelDir })]
350
+ .map(toPosix)
351
+ .filter((file) => file.split("/").length === 2)
352
+ .filter((file) => !isExcluded(file, barrelDir))
353
+ .filter((file) => hasExport(join(barrelDir, file)));
354
+ return [...files, ...childIndexes];
355
+ }
358
356
 
359
- const generatedIndexDirs = new Set(
360
- candidates
361
- .filter((file) => /(^|\/)index\.ts$/.test(file) && isGenerated(join(srcDir, file)))
362
- .map((file) => file.replace(/(^|\/)index\.ts$/, "")),
363
- );
364
- const publicCandidates = candidates.filter((file) => {
365
- if (/(^|\/)index\.ts$/.test(file)) return true;
366
- return ![...generatedIndexDirs].some((dir) => dir && file.startsWith(`${dir}/`));
367
- });
357
+ /** Write or remove one generated barrel and return whether its file changed. */
358
+ function generateBarrel(
359
+ pkgDir: string,
360
+ barrelDir: string,
361
+ moduleFiles: string[],
362
+ includePackageMetadata: boolean,
363
+ ): number {
364
+ const barrel = includePackageMetadata ? join(pkgDir, "index.ts") : join(barrelDir, "index.ts");
365
+ if (!includePackageMetadata && existsSync(barrel) && !isGeneratedBarrel(barrel)) return 0;
366
+ const before = existsSync(barrel) ? readFileSync(barrel, "utf8") : undefined;
368
367
 
369
- // Collapse each extensionless module path to one entry, preferring a TypeScript
370
- // source over a sibling compiled artifact (`math.ts` wins over a committed
371
- // `math.js`), so a module is barrelled exactly once. Then sort by module path.
372
368
  const byModulePath = new Map<string, string>();
373
- for (const f of publicCandidates) {
374
- const stem = f.replace(/(^|\/)index\.ts$/, "").replace(MODULE_EXT_RE, "");
369
+ for (const file of moduleFiles) {
370
+ const stem = file.replace(/(^|\/)index\.ts$/, "").replace(MODULE_EXT_RE, "");
375
371
  const existing = byModulePath.get(stem);
376
- if (!existing || (!isSourceExt(existing) && isSourceExt(f))) byModulePath.set(stem, f);
372
+ if (!existing || (!isSourceExt(existing) && isSourceExt(file))) byModulePath.set(stem, file);
377
373
  }
378
- const modulePaths = [...byModulePath.keys()].sort((a, b) => a.localeCompare(b));
379
- const namespacedModulePaths = modulePaths;
374
+ const modulePaths = [...byModulePath.keys()].sort((left, right) => left.localeCompare(right));
380
375
 
381
- // No eligible modules -> no barrel: drop any stale root barrel and bail.
382
376
  if (modulePaths.length === 0) {
383
- if (existsSync(rootBarrel)) {
384
- makeWritable(rootBarrel);
385
- rmSync(rootBarrel, { force: true });
377
+ if (before !== undefined && (includePackageMetadata || isGeneratedBarrel(barrel))) {
378
+ makeWritable(barrel);
379
+ rmSync(barrel, { force: true });
380
+ return includePackageMetadata ? 0 : 1;
386
381
  }
387
382
  return 0;
388
383
  }
389
384
 
390
- // `./src/<path>` with the module's REAL extension, namespaced by its path
391
- // segments (camelCase; invalid identifiers suffixed with `Module`). The
392
- // extension is written because `tsc` rewrites it on emit
393
- // (`rewriteRelativeImportExtensions`) - an extensionless specifier would be
394
- // copied through verbatim and Node's ESM resolver cannot probe for it.
395
- const namespaceExports = namespacedModulePaths
385
+ const namespaceExports = modulePaths
396
386
  .map((stem) => {
397
- const modulePath = `./src/${byModulePath.get(stem)!}`;
387
+ const prefix = includePackageMetadata ? "./src/" : "./";
388
+ const modulePath = `${prefix}${byModulePath.get(stem)!}`;
398
389
  return `export * as ${modulePathToNamespace(modulePath)} from "${modulePath}";`;
399
390
  })
400
391
  .join("\n");
401
- let content = `${PACKAGE_IDENTIFIER_LINE}\n${PACKAGE_VERSION_LINE}\n${namespaceExports}`;
402
- // Hoist package-unique named exports to the top level. Names a hand-authored
403
- // `exports.ts` declares are suppressed so that file stays authoritative.
404
- const customPath = join(pkgDir, CUSTOM_EXPORTS_FILE);
392
+ let content = includePackageMetadata
393
+ ? `${PACKAGE_IDENTIFIER_LINE}\n${PACKAGE_VERSION_LINE}\n${namespaceExports}`
394
+ : namespaceExports;
395
+ const customDir = includePackageMetadata ? pkgDir : barrelDir;
396
+ const customPath = join(customDir, CUSTOM_EXPORTS_FILE);
405
397
  const suppress = existsSync(customPath) ? customExportNames(customPath) : new Set<string>();
406
- suppress.add(PACKAGE_IDENTIFIER_EXPORT);
407
- suppress.add(PACKAGE_VERSION_EXPORT);
408
- content = hoistUniqueExports(content, pkgDir, suppress);
409
- // A sibling `exports.ts` overrides/extends the generated barrel and wins on conflict.
410
- content = mergeCustomExports(content, pkgDir);
411
-
412
- // Package metadata is blanked for the structural comparison, then restored
413
- // from package.json. If the result matches what's on disk, leave the file and
414
- // its read-only bit untouched and report no change.
398
+ if (includePackageMetadata) {
399
+ suppress.add(PACKAGE_IDENTIFIER_EXPORT);
400
+ suppress.add(PACKAGE_VERSION_EXPORT);
401
+ }
402
+ content = hoistUniqueExports(content, includePackageMetadata ? pkgDir : barrelDir, suppress);
403
+ content = mergeCustomExports(content, customDir);
415
404
  content = `${content.replace(/\n+$/, "")}\n`;
416
405
  const template = `${header(BARREL_HEADER)}\n${content}`;
417
- const next = withPackageMetadata(template, pkgDir);
418
- if (before !== undefined && withoutPackageMetadata(before) === template && before === next) {
419
- return 0;
406
+ const next = includePackageMetadata ? withPackageMetadata(template, pkgDir) : template;
407
+ const structurallySame = includePackageMetadata
408
+ ? before !== undefined && withoutPackageMetadata(before) === template && before === next
409
+ : before === next;
410
+ if (structurallySame) return 0;
411
+
412
+ writeBarrel(barrel, next);
413
+ makeReadonly(barrel);
414
+ return 1;
415
+ }
416
+
417
+ /**
418
+ * Rebuild one package's nested and root barrels. Nested directories are processed
419
+ * deepest-first so a parent can export each child directory through its generated
420
+ * `index.ts`. The package root deliberately continues to export the underlying
421
+ * modules directly, preserving its existing namespaces while adding subpath entrypoints.
422
+ */
423
+ function generateForPackage(pkgDir: string): number {
424
+ const srcDir = join(pkgDir, "src");
425
+ if (!existsSync(srcDir)) return 0;
426
+
427
+ const sourceFiles = [...find.findFiles("**/*", { cwd: srcDir })]
428
+ .map(toPosix)
429
+ .filter((file) => isModuleFile(file) || /(^|\/)index\.ts$/.test(file));
430
+ const manifest = json.parseRecord(readFileSync(join(pkgDir, "package.json"), "utf8"));
431
+ const exports = manifest?.exports;
432
+ const nestedExportsEnabled =
433
+ exports != null &&
434
+ typeof exports === "object" &&
435
+ !Array.isArray(exports) &&
436
+ (exports as Record<string, unknown>)["./*"] === "./src/*/index.ts";
437
+ const directories = new Set<string>();
438
+ for (const file of sourceFiles) {
439
+ const fileDir = dirname(file);
440
+ if (fileDir === "." || /(^|\/)_[^/]+/.test(fileDir)) continue;
441
+ let current = fileDir;
442
+ while (current !== ".") {
443
+ directories.add(current);
444
+ current = dirname(current);
445
+ }
420
446
  }
421
447
 
422
- // Written whole (header included) rather than via `stampGenerated`, which would
423
- // re-read and rewrite the file to prepend the same header - a second write, and
424
- // therefore a second window in which the read-only bit can come back. `next` is
425
- // byte-for-byte what the comparison above accepted, so the two cannot drift.
426
- writeBarrel(rootBarrel, next);
427
- makeReadonly(rootBarrel);
428
- return 1;
448
+ let changed = 0;
449
+ const nestedDirectories = [...directories].sort((left, right) => {
450
+ const depth = (value: string): number => value.split("/").length;
451
+ return depth(right) - depth(left) || right.localeCompare(left);
452
+ });
453
+ for (const directory of nestedDirectories) {
454
+ const barrelDir = join(srcDir, directory);
455
+ if (nestedExportsEnabled) {
456
+ changed += generateBarrel(pkgDir, barrelDir, barrelCandidates(barrelDir), false);
457
+ } else {
458
+ const barrel = join(barrelDir, "index.ts");
459
+ if (isGeneratedBarrel(barrel)) {
460
+ makeWritable(barrel);
461
+ rmSync(barrel, { force: true });
462
+ changed += 1;
463
+ }
464
+ }
465
+ }
466
+
467
+ const candidates = sourceFiles
468
+ .filter((file) => !isGeneratedBarrel(join(srcDir, file)))
469
+ .filter((file) => !isExcluded(file, srcDir))
470
+ .filter((file) => hasExport(join(srcDir, file)));
471
+ const generatedIndexDirs = new Set(
472
+ candidates
473
+ .filter((file) => /(^|\/)index\.ts$/.test(file) && isGenerated(join(srcDir, file)))
474
+ .map((file) => file.replace(/(^|\/)index\.ts$/, "")),
475
+ );
476
+ const publicCandidates = candidates.filter((file) => {
477
+ if (/(^|\/)index\.ts$/.test(file)) return true;
478
+ return ![...generatedIndexDirs].some((dir) => dir && file.startsWith(`${dir}/`));
479
+ });
480
+
481
+ return changed + generateBarrel(pkgDir, srcDir, publicCandidates, true);
429
482
  }
430
483
 
431
484
  /**
package/src/project-js.ts CHANGED
@@ -15,7 +15,7 @@ import { type IConstruct } from "constructs";
15
15
  import { Component, LogLevel, Project, type Task, javascript, typescript } from "projen";
16
16
  import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
17
17
  import type { ReleaseProjectOptions } from "projen/lib/release";
18
- import { generateBarrels } from "./barrels.ts";
18
+ import { generateBarrels, isGeneratedBarrel } from "./barrels.ts";
19
19
 
20
20
  import { BUN_APP_OVERRIDES, RootBunfigFile } from "./bun-app.ts";
21
21
  import { BUN_VERSION } from "./bun-workflow.ts";
@@ -288,6 +288,8 @@ export function addPackageFiles(pkg: javascript.NodeProject, ...entries: string[
288
288
  * non-`_` module, so those names are public through `.` either way - the subpaths
289
289
  * just add a narrower import path. Deriving the map is what lets a tag carry the
290
290
  * whole export layout, instead of each package hand-listing its own modules.
291
+ * The wildcard resolves generated nested barrels at every depth while exact
292
+ * exports continue to win for hand-authored entrypoints and assets.
291
293
  */
292
294
  export function srcModuleExports(pkg: javascript.NodeProject): Record<string, string> {
293
295
  const srcDir = join(pkg.outdir, "src");
@@ -310,9 +312,11 @@ export function srcModuleExports(pkg: javascript.NodeProject): Record<string, st
310
312
  const index = ["index.ts", "index.tsx"].find((file) =>
311
313
  existsSync(join(srcDir, entry.name, file)),
312
314
  );
313
- if (index) exports[`./${entry.name}`] = `./src/${entry.name}/${index}`;
315
+ if (index && !isGeneratedBarrel(join(srcDir, entry.name, index))) {
316
+ exports[`./${entry.name}`] = `./src/${entry.name}/${index}`;
317
+ }
314
318
  }
315
- return exports;
319
+ return { ...exports, "./*": "./src/*/index.ts" };
316
320
  }
317
321
 
318
322
  /**
@@ -883,11 +887,15 @@ class GeneratedSource extends Component {
883
887
  const sourceExports = Object.fromEntries(
884
888
  modules.map((module) => [`./${module.slice("src/".length, -".ts".length)}`, `./${module}`]),
885
889
  );
890
+ const nestedExport = current["./*"];
886
891
  subproject.package.addField("exports", {
887
892
  ...Object.fromEntries(
888
- Object.entries(current).filter(([subpath]) => subpath !== "./package.json"),
893
+ Object.entries(current).filter(
894
+ ([subpath]) => subpath !== "./*" && subpath !== "./package.json",
895
+ ),
889
896
  ),
890
897
  ...sourceExports,
898
+ ...(nestedExport ? { "./*": nestedExport } : {}),
891
899
  ...(current["./package.json"] ? { "./package.json": current["./package.json"] } : {}),
892
900
  });
893
901
  }
package/tasks/publish.ts CHANGED
@@ -88,6 +88,12 @@ function compiledPublishTargets(value: unknown): string[] {
88
88
  return Object.values(value).flatMap(compiledPublishTargets);
89
89
  }
90
90
 
91
+ export function compiledPublishTargetExists(dir: string, target: string): boolean {
92
+ if (!target.includes("*")) return existsSync(resolve(dir, target));
93
+ const staticRoot = target.slice(0, target.indexOf("*")).replace(/\/+$/, "");
94
+ return existsSync(resolve(dir, staticRoot));
95
+ }
96
+
91
97
  /**
92
98
  * PATH with the workspace-root `node_modules/.bin` prepended. The root-level
93
99
  * compile reaches each package's projen/dax task, which resolves `tsc` off PATH;
@@ -199,7 +205,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
199
205
  if (skipCompile) {
200
206
  const missing = compiled.flatMap((pkg) =>
201
207
  pkg.compiledTargets
202
- .filter((target) => !existsSync(resolve(pkg.dir, target)))
208
+ .filter((target) => !compiledPublishTargetExists(pkg.dir, target))
203
209
  .map((target) => `${pkg.name}:${target}`),
204
210
  );
205
211
  if (missing.length > 0) {