@dbx-tools/projen 0.4.1 → 0.5.0

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/index.ts CHANGED
@@ -2,55 +2,95 @@
2
2
  // Regenerated from the exporting modules in ./src.
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
- export * as barrels from "./src/barrels";
6
- export * as clean from "./src/clean";
7
- export * as cliBin from "./src/cli-bin";
8
- export * as codegen from "./src/codegen";
9
- export * as dbxToolsConfig from "./src/dbx-tools-config";
10
- export * as engineRoot from "./src/engine-root";
11
- export * as generated from "./src/generated";
12
- export * as mixin from "./src/mixin";
13
- export * as moduleExports from "./src/module-exports";
14
- export * as openapi from "./src/openapi";
15
- export * as packages from "./src/packages";
16
- export * as pnpmWorkspace from "./src/pnpm-workspace";
17
- export * as project from "./src/project";
18
- export * as projectPredicate from "./src/project-predicate";
19
- export * as release from "./src/release";
20
- export * as scaffold from "./src/scaffold";
21
- export * as tags from "./src/tags";
22
- export * as tsconfig from "./src/tsconfig";
23
- export * as vite from "./src/vite";
24
- export * as vscode from "./src/vscode";
25
- export * as watch from "./src/watch";
26
- export { generateBarrels } from "./src/barrels";
27
- export { listGeneratedFiles, listNodeModulesDirs, removePaths } from "./src/clean";
28
- export { CliBinLauncher, addCliBinLaunchers } from "./src/cli-bin";
29
- export { generateCodegen } from "./src/codegen";
30
- export { DBXToolsConfig } from "./src/dbx-tools-config";
31
- export type { DBXToolsConfigOptions } from "./src/dbx-tools-config";
32
- export { resolvePkgRoot } from "./src/engine-root";
33
- export { makeWritable, makeReadonly, isReadonly, header, stampGenerated } from "./src/generated";
34
- export type { HeaderOpts } from "./src/generated";
35
- export { create } from "./src/mixin";
36
- export type { ConstructsMixin } from "./src/mixin";
37
- export { moduleStatements } from "./src/module-exports";
38
- export type { ModuleStatement, ModuleExport } from "./src/module-exports";
39
- export { isTsoaController, generateOpenapi } from "./src/openapi";
40
- export { repoRoot, DEFAULT_PACKAGE_ROOTS, projectName, toPosix, isGeneratedFile, isModuleFile, DiscoveredPackage, scanPackages, readPackageManifest, syncResynthPaths, recordedPackages, recordedRoots } from "./src/packages";
41
- export type { RecordedPackage } from "./src/packages";
42
- export { PnpmWorkspaceState } from "./src/pnpm-workspace";
43
- export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace";
44
- export { PackageIdentifier, identifier, applyCompilerOptions, applyIncludes, applyTasks, applyExports, addExports, addPackageFiles, srcModuleExports, PROJEN_VERSION, DBXToolsNodeProject, DBXToolsTypeScriptProject, taskScript, applyToProjects } from "./src/project";
45
- export type { DBXToolsProject, DBXToolsProjectOptions, DBXToolsTypeScriptProjectOptions, ApplyToProjectsOptions } from "./src/project";
46
- export { isProject, isDBXToolsProject, hasName, hasIdentifierPackageName, hasIdentifierName, hasIdentifierScope, hasTag, hasPath } from "./src/project-predicate";
47
- export { DBXToolsRelease } from "./src/release";
48
- export type { StandaloneRelease, DBXToolsReleaseOptions } from "./src/release";
49
- export { runSynth } from "./src/scaffold";
50
- export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags";
51
- export type { PackageTag } from "./src/tags";
52
- export { DBXToolsRootTsconfig } from "./src/tsconfig";
53
- export { ViteConfigFile } from "./src/vite";
54
- export { DBXToolsVsCode } from "./src/vscode";
55
- export { watchRoots, watchLoop } from "./src/watch";
56
- export type { IgnoreGroupOptions } from "./src/watch";
5
+ export * as barrels from "./src/barrels.ts";
6
+ export * as clean from "./src/clean.ts";
7
+ export * as codegen from "./src/codegen.ts";
8
+ export * as dbxToolsConfig from "./src/dbx-tools-config.ts";
9
+ export * as engineRoot from "./src/engine-root.ts";
10
+ export * as generated from "./src/generated.ts";
11
+ export * as mixin from "./src/mixin.ts";
12
+ export * as moduleExports from "./src/module-exports.ts";
13
+ export * as openapi from "./src/openapi.ts";
14
+ export * as packages from "./src/packages.ts";
15
+ export * as pnpmWorkspace from "./src/pnpm-workspace.ts";
16
+ export * as project from "./src/project.ts";
17
+ export * as projectPredicate from "./src/project-predicate.ts";
18
+ export * as release from "./src/release.ts";
19
+ export * as scaffold from "./src/scaffold.ts";
20
+ export * as tags from "./src/tags.ts";
21
+ export * as tsconfig from "./src/tsconfig.ts";
22
+ export * as vite from "./src/vite.ts";
23
+ export * as vscode from "./src/vscode.ts";
24
+ export * as watch from "./src/watch.ts";
25
+ export { generateBarrels } from "./src/barrels.ts";
26
+ export { listGeneratedFiles, listNodeModulesDirs, removePaths } from "./src/clean.ts";
27
+ export { generateCodegen } from "./src/codegen.ts";
28
+ export { DBXToolsConfig } from "./src/dbx-tools-config.ts";
29
+ export type { DBXToolsConfigOptions } from "./src/dbx-tools-config.ts";
30
+ export { resolvePkgRoot } from "./src/engine-root.ts";
31
+ export { makeWritable, makeReadonly, isReadonly, header, stampGenerated } from "./src/generated.ts";
32
+ export type { HeaderOpts } from "./src/generated.ts";
33
+ export { create } from "./src/mixin.ts";
34
+ export type { ConstructsMixin } from "./src/mixin.ts";
35
+ export { moduleStatements } from "./src/module-exports.ts";
36
+ export type { ModuleStatement, ModuleExport } from "./src/module-exports.ts";
37
+ export { isTsoaController, generateOpenapi } from "./src/openapi.ts";
38
+ export {
39
+ repoRoot,
40
+ DEFAULT_PACKAGE_ROOTS,
41
+ projectName,
42
+ toPosix,
43
+ isGeneratedFile,
44
+ isModuleFile,
45
+ DiscoveredPackage,
46
+ scanPackages,
47
+ readPackageManifest,
48
+ syncResynthPaths,
49
+ recordedPackages,
50
+ recordedRoots,
51
+ } from "./src/packages.ts";
52
+ export type { RecordedPackage } from "./src/packages.ts";
53
+ export { PnpmWorkspaceState } from "./src/pnpm-workspace.ts";
54
+ export type { Catalog, AllowBuilds, DBXToolsPNPMWorkspaceOptions } from "./src/pnpm-workspace.ts";
55
+ export {
56
+ PackageIdentifier,
57
+ identifier,
58
+ applyCompilerOptions,
59
+ applyIncludes,
60
+ applyTasks,
61
+ applyExports,
62
+ addExports,
63
+ addPackageFiles,
64
+ srcModuleExports,
65
+ PROJEN_VERSION,
66
+ DBXToolsNodeProject,
67
+ DBXToolsTypeScriptProject,
68
+ taskScript,
69
+ applyToProjects,
70
+ } from "./src/project.ts";
71
+ export type {
72
+ DBXToolsProject,
73
+ DBXToolsProjectOptions,
74
+ DBXToolsTypeScriptProjectOptions,
75
+ ApplyToProjectsOptions,
76
+ } from "./src/project.ts";
77
+ export {
78
+ isProject,
79
+ isDBXToolsProject,
80
+ hasName,
81
+ hasIdentifierPackageName,
82
+ hasIdentifierName,
83
+ hasIdentifierScope,
84
+ hasTag,
85
+ hasPath,
86
+ } from "./src/project-predicate.ts";
87
+ export { DBXToolsRelease } from "./src/release.ts";
88
+ export type { StandaloneRelease, DBXToolsReleaseOptions } from "./src/release.ts";
89
+ export { runSynth } from "./src/scaffold.ts";
90
+ export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
91
+ export type { PackageTag } from "./src/tags.ts";
92
+ export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
93
+ export { ViteConfigFile } from "./src/vite.ts";
94
+ export { DBXToolsVsCode } from "./src/vscode.ts";
95
+ export { watchRoots, watchLoop } from "./src/watch.ts";
96
+ export type { IgnoreGroupOptions } from "./src/watch.ts";
package/package.json CHANGED
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "main": "index.ts",
34
34
  "license": "Apache-2.0",
35
- "version": "0.4.1",
35
+ "version": "0.5.0",
36
36
  "types": "index.ts",
37
37
  "type": "module",
38
38
  "exports": {
package/src/barrels.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * takes priority. This keeps the barrel auto-generated while letting you add or
16
16
  * override individual exports.
17
17
  *
18
- * Each eligible module becomes `export * as <name> from "./src/x"` (camelCase
18
+ * Each eligible module becomes `export * as <name> from "./src/x.ts"` (camelCase
19
19
  * namespace from its path segments; invalid identifiers suffixed with `Module`),
20
20
  * sorted by module path.
21
21
  *
@@ -40,9 +40,15 @@ import { join } from "node:path";
40
40
  import { find } from "@dbx-tools/path";
41
41
  import { string } from "@dbx-tools/shared-core";
42
42
  import isIdentifier from "is-identifier";
43
- import { header, makeReadonly, makeWritable, stampGenerated, type HeaderOpts } from "./generated";
44
- import { moduleExports, moduleStatements, type ModuleExport } from "./module-exports";
45
- import { isModuleFile, toPosix, recordedPackages } from "./packages";
43
+ import {
44
+ header,
45
+ makeReadonly,
46
+ makeWritable,
47
+ stampGenerated,
48
+ type HeaderOpts,
49
+ } from "./generated.ts";
50
+ import { moduleExports, moduleStatements, type ModuleExport } from "./module-exports.ts";
51
+ import { isModuleFile, toPosix, recordedPackages } from "./packages.ts";
46
52
 
47
53
  /**
48
54
  * A `src`-relative posix path excluded from the root barrel:
@@ -149,8 +155,7 @@ function hoistUniqueExports(content: string, pkgDir: string, suppress: Set<strin
149
155
  const seen = new Map<string, { count: number; modulePath: string }>();
150
156
  const perModule = new Map<string, ModuleExport[]>();
151
157
  for (const { modulePath } of namespaces) {
152
- const abs = join(pkgDir, modulePath.replace(/^\.\//, ""));
153
- const exports = moduleExports(withTsExt(abs));
158
+ const exports = moduleExports(join(pkgDir, modulePath.replace(/^\.\//, "")));
154
159
  perModule.set(modulePath, exports);
155
160
  for (const { name } of exports) {
156
161
  const prior = seen.get(name);
@@ -177,14 +182,6 @@ function hoistUniqueExports(content: string, pkgDir: string, suppress: Set<strin
177
182
  return `${content.replace(/\n+$/, "")}\n${lines.join("\n")}\n`;
178
183
  }
179
184
 
180
- /** Resolve a barrel module path (`./src/x`, extensionless) to its on-disk `.ts(x)` file. */
181
- function withTsExt(absNoExt: string): string {
182
- for (const ext of [".ts", ".tsx", ".mts", ".cts"]) {
183
- if (existsSync(absNoExt + ext)) return absNoExt + ext;
184
- }
185
- return `${absNoExt}.ts`;
186
- }
187
-
188
185
  /** Hand-authored override barrel: a sibling of the generated `index.ts`. */
189
186
  const CUSTOM_EXPORTS_FILE = "exports.ts";
190
187
 
@@ -229,7 +226,7 @@ function mergeCustomExports(content: string, pkgDir: string): string {
229
226
  const ns = /^export \* as (\w+) from /.exec(line)?.[1];
230
227
  return !(ns && overridden.has(ns));
231
228
  });
232
- return `${kept.join("\n").replace(/\n+$/, "")}\nexport * from "./exports";\n`;
229
+ return `${kept.join("\n").replace(/\n+$/, "")}\nexport * from "./exports.ts";\n`;
233
230
  }
234
231
 
235
232
  /**
@@ -285,11 +282,14 @@ function generateForPackage(pkgDir: string): number {
285
282
  // Unlock the read-only barrel so the rewrite below can replace it.
286
283
  makeWritable(rootBarrel);
287
284
 
288
- // `./src/<path-without-ext>` namespaced by its path segments (camelCase; invalid
289
- // identifiers suffixed with `Module`).
285
+ // `./src/<path>` with the module's REAL extension, namespaced by its path
286
+ // segments (camelCase; invalid identifiers suffixed with `Module`). The
287
+ // extension is written because `tsc` rewrites it on emit
288
+ // (`rewriteRelativeImportExtensions`) - an extensionless specifier would be
289
+ // copied through verbatim and Node's ESM resolver cannot probe for it.
290
290
  let content = modulePaths
291
291
  .map((stem) => {
292
- const modulePath = `./src/${stem}`;
292
+ const modulePath = `./src/${byModulePath.get(stem)!}`;
293
293
  return `export * as ${modulePathToNamespace(modulePath)} from "${modulePath}";`;
294
294
  })
295
295
  .join("\n");
package/src/clean.ts CHANGED
@@ -24,8 +24,8 @@
24
24
  import { existsSync, rmSync, statSync } from "node:fs";
25
25
  import { basename, join, relative } from "node:path";
26
26
  import { find } from "@dbx-tools/path";
27
- import { isReadonly, makeWritable } from "./generated";
28
- import { repoRoot, toPosix } from "./packages";
27
+ import { isReadonly, makeWritable } from "./generated.ts";
28
+ import { repoRoot, toPosix } from "./packages.ts";
29
29
 
30
30
  /**
31
31
  * Basenames `clean` never removes even when they are generated/read-only. `.gitignore`
package/src/codegen.ts CHANGED
@@ -47,10 +47,10 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
47
47
  import { createRequire } from "node:module";
48
48
  import { basename, dirname, join, resolve } from "node:path";
49
49
  import type * as ts from "typescript";
50
- import { lazyRequire } from "./_lazy-require";
51
- import { header, isReadonly, makeReadonly, makeWritable } from "./generated";
50
+ import { lazyRequire } from "./_lazy-require.ts";
51
+ import { header, isReadonly, makeReadonly, makeWritable } from "./generated.ts";
52
52
  import { log, object } from "@dbx-tools/shared-core";
53
- import { readPackageManifest, repoRoot, recordedPackages } from "./packages";
53
+ import { readPackageManifest, repoRoot, recordedPackages } from "./packages.ts";
54
54
 
55
55
  const logger = log.logger("projen:codegen");
56
56
 
@@ -322,8 +322,12 @@ export function generateCodegen(): string[] {
322
322
  .map((p) => ({ dir: p.dir, inputs: codegenInputs(p.dir) }))
323
323
  .filter((t): t is { dir: string; inputs: string[] } => t.inputs !== undefined);
324
324
 
325
+ // Nothing to generate is the NORMAL case for a workspace that declares no
326
+ // `codegen` inputs, and this runs on every synth - so it stays silent rather
327
+ // than printing a line about work that was never asked for. `debug` keeps it
328
+ // reachable when someone is actually chasing why codegen produced nothing.
325
329
  if (targets.length === 0) {
326
- logger.info("no packages declare a `codegen` field");
330
+ logger.debug("no packages declare a `codegen` field");
327
331
  return [];
328
332
  }
329
333
 
package/src/openapi.ts CHANGED
@@ -25,8 +25,8 @@ import { createRequire } from "node:module";
25
25
  import { join } from "node:path";
26
26
  import type * as ts from "typescript";
27
27
  import { find } from "@dbx-tools/path";
28
- import { lazyRequire } from "./_lazy-require";
29
- import { makeReadonly, makeWritable, stampGenerated } from "./generated";
28
+ import { lazyRequire } from "./_lazy-require.ts";
29
+ import { makeReadonly, makeWritable, stampGenerated } from "./generated.ts";
30
30
  import { log } from "@dbx-tools/shared-core";
31
31
  import {
32
32
  type RecordedPackage,
@@ -34,7 +34,7 @@ import {
34
34
  repoRoot,
35
35
  toPosix,
36
36
  recordedPackages,
37
- } from "./packages";
37
+ } from "./packages.ts";
38
38
 
39
39
  const logger = log.logger("projen:openapi");
40
40
 
@@ -86,15 +86,21 @@ export function isTsoaController(path: string): boolean {
86
86
  */
87
87
  export async function generateOpenapi(): Promise<string[]> {
88
88
  const pkgs = controllerPackages();
89
+ // Same reasoning as codegen's empty case: a workspace with no tsoa controllers
90
+ // is not a condition worth a line on every synth.
89
91
  if (pkgs.length === 0) {
90
- logger.info("no tsoa controllers found in any server/node package");
92
+ logger.debug("no tsoa controllers found in any server/node package");
91
93
  return [];
92
94
  }
93
95
 
94
96
  // Lazy, resilient loads: tsoa + typescript are CJS (require), openapi-typescript
95
97
  // is ESM (dynamic import).
96
98
  const require = createRequire(import.meta.url);
97
- const { generateSpec } = lazyRequire<typeof import("tsoa")>(require, "tsoa", "openapi generation");
99
+ const { generateSpec } = lazyRequire<typeof import("tsoa")>(
100
+ require,
101
+ "tsoa",
102
+ "openapi generation",
103
+ );
98
104
  const tsRuntime = lazyRequire<typeof ts>(require, "typescript", "openapi generation");
99
105
  const { default: openapiTS, astToString } = await import("openapi-typescript");
100
106
 
@@ -40,7 +40,7 @@
40
40
  */
41
41
  import { relative } from "node:path";
42
42
  import { javascript, type Project } from "projen";
43
- import { toPosix } from "./packages";
43
+ import { toPosix } from "./packages.ts";
44
44
 
45
45
  /**
46
46
  * The pnpm `catalog:` version registry: dependency name -> version range. A
@@ -7,8 +7,8 @@ import {
7
7
  } from "@dbx-tools/shared-core";
8
8
  import { IConstruct } from "constructs";
9
9
  import { Project } from "projen";
10
- import { DBXToolsProject, DBXToolsNodeProject, DBXToolsTypeScriptProject } from "./project";
11
- import { toPosix } from "./packages";
10
+ import { DBXToolsProject, DBXToolsNodeProject, DBXToolsTypeScriptProject } from "./project.ts";
11
+ import { toPosix } from "./packages.ts";
12
12
  import { relative } from "node:path";
13
13
  import { match, PathMatchInput, PathMatchPredicate } from "@dbx-tools/path";
14
14
  import { project } from "..";
package/src/project.ts CHANGED
@@ -13,17 +13,17 @@ import { existsSync, readdirSync } from "node:fs";
13
13
  import { dirname, join, relative, resolve } from "node:path";
14
14
  import { Component, IgnoreFile, Project, type TaskOptions, javascript, typescript } from "projen";
15
15
  import { ReleaseTrigger } from "projen/lib/release";
16
- import { generateBarrels } from "./barrels";
17
- import { generateCodegen } from "./codegen";
18
- import { DBXToolsConfig, type DBXToolsConfigOptions } from "./dbx-tools-config";
19
- import { resolvePkgRoot } from "./engine-root";
20
- import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace";
21
- import { applyCompiledPublish } from "./publish";
22
- import { DBXToolsRelease, type StandaloneRelease } from "./release";
23
- import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags";
24
- import { DBXToolsRootTsconfig } from "./tsconfig";
25
- import { ViteConfigFile } from "./vite";
26
- import { DBXToolsVsCode } from "./vscode";
16
+ import { generateBarrels } from "./barrels.ts";
17
+ import { generateCodegen } from "./codegen.ts";
18
+ import { DBXToolsConfig, type DBXToolsConfigOptions } from "./dbx-tools-config.ts";
19
+ import { resolvePkgRoot } from "./engine-root.ts";
20
+ import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace.ts";
21
+ import { applyCompiledPublish } from "./publish.ts";
22
+ import { DBXToolsRelease, type StandaloneRelease } from "./release.ts";
23
+ import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags.ts";
24
+ import { DBXToolsRootTsconfig } from "./tsconfig.ts";
25
+ import { ViteConfigFile } from "./vite.ts";
26
+ import { DBXToolsVsCode } from "./vscode.ts";
27
27
  import {
28
28
  DEFAULT_PACKAGE_ROOTS,
29
29
  type DiscoveredPackage,
@@ -32,7 +32,7 @@ import {
32
32
  repoRoot,
33
33
  scanPackages,
34
34
  toPosix,
35
- } from "./packages";
35
+ } from "./packages.ts";
36
36
  import { mixin, projectPredicate } from "..";
37
37
  import { IConstruct } from "constructs";
38
38
 
@@ -271,11 +271,25 @@ export function srcModuleExports(pkg: javascript.NodeProject): Record<string, st
271
271
  return exports;
272
272
  }
273
273
 
274
- /** ESM compiler options every Node package shares regardless of tag. */
275
- const SHARED_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
274
+ /**
275
+ * ESM compiler options every package shares regardless of tag.
276
+ *
277
+ * Relative imports in this repo carry their REAL extension (`./http.ts`), which
278
+ * is what lets `tsc` rewrite them to `./http.js` on emit
279
+ * (`rewriteRelativeImportExtensions`) instead of a post-processing pass fixing up
280
+ * the emitted tree. Both flags belong here rather than on the publishing packages
281
+ * only: the specifier style is a property of the SOURCE, so a `ui` package (which
282
+ * publishes source and is excluded from the compiled surface) still has to accept
283
+ * and rewrite it.
284
+ */
285
+ const SHARED_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions & {
286
+ rewriteRelativeImportExtensions: boolean;
287
+ } = {
276
288
  module: "ESNext",
277
289
  moduleResolution: javascript.TypeScriptModuleResolution.BUNDLER,
278
290
  skipLibCheck: true,
291
+ allowImportingTsExtensions: true,
292
+ rewriteRelativeImportExtensions: true,
279
293
  };
280
294
 
281
295
  /** Shared formatting rules, applied by projen's Prettier on whichever project is root. */
@@ -827,7 +841,6 @@ function initProject(
827
841
  for (const root of roots) {
828
842
  project.annotateGenerated(`/${root}/**/index.ts`);
829
843
  project.annotateGenerated(`/${root}/openapi/**`);
830
- project.annotateGenerated(`/${root}/**/bin/*.mjs`);
831
844
  }
832
845
 
833
846
  // ESLint lives ONLY on the root and lints every package. `projectService` resolves
package/src/publish.ts CHANGED
@@ -4,44 +4,52 @@
4
4
  *
5
5
  * Packages resolve each other from SOURCE - every `exports` entry points at a
6
6
  * `.ts` file, so a cross-package import type-checks with no build step and no
7
- * `dist` to keep in sync. That property is worth keeping, but it cannot be what
7
+ * `lib` to keep in sync. That property is worth keeping, but it cannot be what
8
8
  * ships: Node refuses to strip types under `node_modules`
9
9
  * (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so a published package whose
10
10
  * entry point is `index.ts` is unloadable by anything that is not a bundler.
11
- * Consumers papered over that by special-casing `@dbx-tools/*` into their own
12
- * bundle, which is a tax this repo has no business charging.
13
11
  *
14
12
  * pnpm resolves both at once. It substitutes `publishConfig`'s `main`/`types`/
15
- * `exports` into the manifest at pack time and drops `publishConfig` itself, so
16
- * the workspace keeps its source entry points while the tarball advertises the
17
- * compiled ones. Nothing here changes how the repo builds or type-checks; it
18
- * only changes what `pnpm pack` writes.
13
+ * `bin`/`exports` into the manifest at pack time and drops `publishConfig`
14
+ * itself, so the workspace keeps its source entry points while the tarball
15
+ * advertises the compiled ones. Nothing here changes how the repo builds or
16
+ * type-checks; it only changes what `pnpm pack` writes.
19
17
  *
20
- * Two pieces make the emitted tree actually loadable:
18
+ * Two compiler options make the emitted tree loadable, both native `tsc`:
21
19
  *
22
20
  * - `rootDir: "."` so the package-ROOT `index.ts` barrel is compiled at all.
23
21
  * projen's default `rootDir: "src"` puts the barrel outside the compilation,
24
- * which is why `lib/` has never had an `index.js`.
25
- * - a specifier pass after `tsc`, because `tsc` never rewrites import paths.
26
- * Sources are written for `moduleResolution: bundler` and so import `"./http"`,
27
- * which Node cannot resolve; `tasks/emit.ts` appends the extension that the
28
- * emitted file actually has. Doing it after the fact keeps 800-odd import
29
- * sites free of the `.js` suffix that would otherwise have to be written - and
30
- * maintained - by hand.
22
+ * which is why `lib/` would otherwise have no `index.js`.
23
+ * - `rewriteRelativeImportExtensions`, which turns the `./x.ts` specifiers this
24
+ * repo writes into the `./x.js` Node's ESM resolver needs. Sources carry the
25
+ * real extension (`allowImportingTsExtensions`) and `tsc` rewrites it on emit,
26
+ * so no post-processing pass is involved.
31
27
  *
32
28
  * UI packages are deliberately excluded (see {@link publishesCompiled}).
33
29
  */
34
30
  import type { javascript } from "projen";
35
31
  import { typescript } from "projen";
36
- import { addPackageFiles, applyCompilerOptions, applyIncludes, taskScript } from "./project";
37
- import { isDBXToolsProject } from "./project-predicate";
32
+ import { addPackageFiles, applyCompilerOptions, applyIncludes } from "./project.ts";
33
+ import { isDBXToolsProject } from "./project-predicate.ts";
38
34
 
39
35
  /** Directory `tsc` emits into, and the root of every published entry point. */
40
36
  export const COMPILED_DIR = "lib";
41
37
 
42
- /** An `exports` target written as TypeScript source, i.e. one with a compiled twin. */
38
+ /** An `exports`/`bin` target written as TypeScript source, i.e. with a compiled twin. */
43
39
  const TS_SOURCE = /\.tsx?$/;
44
40
 
41
+ /**
42
+ * Compiler options that make a package's emitted `lib/` tree loadable by Node.
43
+ *
44
+ * Only `rootDir` here: the specifier rewriting
45
+ * (`allowImportingTsExtensions` + `rewriteRelativeImportExtensions`) is part of
46
+ * the tsconfig floor EVERY package gets, since the `.ts`-suffixed specifier style
47
+ * is a property of the source rather than of publishing.
48
+ */
49
+ export const COMPILED_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
50
+ rootDir: ".",
51
+ };
52
+
45
53
  /**
46
54
  * Whether a package publishes compiled output rather than source.
47
55
  *
@@ -52,15 +60,16 @@ const TS_SOURCE = /\.tsx?$/;
52
60
  * `./styles.css` plus raw SVG assets that `tsc` does not copy and could not
53
61
  * rewrite. Compiling them would mean a real asset pipeline (Vite library mode)
54
62
  * to solve a problem they do not have.
55
- *
56
- * This is the same split the downstream app build already makes on its own: its
57
- * client bundle resolves `@dbx-tools/ui-*` from source without complaint, while
58
- * its SERVER bundle is the one that had to inline `@dbx-tools/*` to avoid
59
- * handing Node a `.ts` entry point.
60
63
  */
61
64
  export function publishesCompiled(pkg: javascript.NodeProject): boolean {
62
65
  if (!(pkg instanceof typescript.TypeScriptProject) || !pkg.parent) return false;
63
- return isDBXToolsProject(pkg) && !pkg.dbxToolsConfig.tags.includes("ui");
66
+ return isDBXToolsProject()(pkg) && !pkg.dbxToolsConfig.tags.includes("ui");
67
+ }
68
+
69
+ /** The `./lib/...` stem of a source path, or `undefined` if it is not TypeScript. */
70
+ function compiledStem(target: string): string | undefined {
71
+ if (!TS_SOURCE.test(target)) return undefined;
72
+ return `./${COMPILED_DIR}/${target.replace(/^\.\//, "").replace(TS_SOURCE, "")}`;
64
73
  }
65
74
 
66
75
  /**
@@ -71,13 +80,12 @@ export function publishesCompiled(pkg: javascript.NodeProject): boolean {
71
80
  * mapping is positional: `./src/react/index.ts` -> `./lib/src/react/index.js`.
72
81
  */
73
82
  function compiledTarget(target: string): { types: string; default: string } | undefined {
74
- if (!TS_SOURCE.test(target)) return undefined;
75
- const stem = `./${COMPILED_DIR}/${target.replace(/^\.\//, "").replace(TS_SOURCE, "")}`;
76
- return { types: `${stem}.d.ts`, default: `${stem}.js` };
83
+ const stem = compiledStem(target);
84
+ return stem ? { types: `${stem}.d.ts`, default: `${stem}.js` } : undefined;
77
85
  }
78
86
 
79
87
  /**
80
- * Derive `publishConfig` from the package's FINAL `exports` map.
88
+ * Derive `publishConfig` from the package's FINAL `exports` + `bin` maps.
81
89
  *
82
90
  * Runs at preSynthesize precisely so the tags have already installed their
83
91
  * export layouts - deriving it any earlier would mirror the constructor's bare
@@ -88,9 +96,26 @@ function publishConfig(pkg: javascript.NodeProject): Record<string, unknown> | u
88
96
  const compiled = Object.entries(exports).map(
89
97
  ([subpath, target]) => [subpath, compiledTarget(target) ?? target] as const,
90
98
  );
99
+
100
+ // A CLI's `bin` points at its `.ts` entry in-repo (Node strips types outside
101
+ // `node_modules`, so a workspace checkout runs it directly); the tarball has to
102
+ // point at the emitted `.js`, which is what lets the published CLI run with no
103
+ // loader installed.
104
+ // `bin` is rendered LAZILY (projen assigns `bin: () => this.renderBin()` and
105
+ // keeps the map itself private), so unlike `exports` this has to be called -
106
+ // reading the field directly yields the function and finds no entries.
107
+ const binField = pkg.package.manifest.bin as
108
+ Record<string, string> | (() => Record<string, string>) | undefined;
109
+ const bin = (typeof binField === "function" ? binField() : binField) ?? {};
110
+ const compiledBin = Object.entries(bin).flatMap(([name, target]) => {
111
+ const stem = compiledStem(target);
112
+ return stem ? [[name, `${stem}.js`] as const] : [];
113
+ });
114
+
91
115
  // Nothing to swap means the package ships no TypeScript entry point at all;
92
116
  // leave its manifest alone rather than writing an inert `publishConfig`.
93
- if (!compiled.some(([, target]) => typeof target !== "string")) return undefined;
117
+ const swaps = compiled.some(([, target]) => typeof target !== "string");
118
+ if (!swaps && compiledBin.length === 0) return undefined;
94
119
 
95
120
  // Setting this field REPLACES whatever projen renders into it, and what projen
96
121
  // renders is `access` - dropped, every scoped package here would publish as
@@ -100,13 +125,14 @@ function publishConfig(pkg: javascript.NodeProject): Record<string, unknown> | u
100
125
  return {
101
126
  access: pkg.package.npmAccess,
102
127
  ...(typeof root === "object" ? { main: root.default, types: root.types } : {}),
128
+ ...(compiledBin.length ? { bin: Object.fromEntries(compiledBin) } : {}),
103
129
  exports: Object.fromEntries(compiled),
104
130
  };
105
131
  }
106
132
 
107
133
  /**
108
- * Give a package a compiled publish surface: emit the barrel, fix the emitted
109
- * specifiers, ship `lib/`, and advertise it through `publishConfig`.
134
+ * Give a package a compiled publish surface: emit the barrel, ship `lib/`, and
135
+ * advertise it through `publishConfig`.
110
136
  *
111
137
  * Idempotent - `preSynthesizeProject` reaches every package twice (once from the
112
138
  * root's walk, once from the package's own preSynthesize) and both passes must
@@ -115,22 +141,13 @@ function publishConfig(pkg: javascript.NodeProject): Record<string, unknown> | u
115
141
  export function applyCompiledPublish(pkg: javascript.NodeProject): void {
116
142
  if (!publishesCompiled(pkg)) return;
117
143
 
118
- // The barrel lives at the package root, so the compilation has to start there.
119
- // The `cli` tag already does this for its `bin/` tree; for everything else it
120
- // is what puts an `index.js` in the emitted output for the first time.
121
- applyCompilerOptions(pkg, { rootDir: "." });
144
+ applyCompilerOptions(pkg, COMPILED_COMPILER_OPTIONS);
122
145
  applyIncludes(pkg, "index.ts");
123
146
  addPackageFiles(pkg, COMPILED_DIR);
124
147
 
125
148
  const config = publishConfig(pkg);
126
149
  if (config) pkg.package.addField("publishConfig", config);
127
150
 
128
- // `tsc` emits extensionless relative specifiers; Node ESM cannot resolve them.
129
- const fixSpecifiers = taskScript(pkg, "emit.ts", COMPILED_DIR);
130
- if (!pkg.compileTask.steps.some((step) => step.exec === fixSpecifiers)) {
131
- pkg.compileTask.exec(fixSpecifiers);
132
- }
133
-
134
151
  // The release workflow publishes straight after `pnpm install`, with no build
135
152
  // in between, so the compiled output has to be produced by the pack itself
136
153
  // rather than assumed present. This also covers a bare `pnpm pack` and the
package/src/release.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { Component } from "projen";
8
8
  import { GithubWorkflow } from "projen/lib/github";
9
9
  import { JobPermission } from "projen/lib/github/workflows-model";
10
- import { applyTasks, taskScript, type DBXToolsNodeProject } from "./project";
10
+ import { applyTasks, taskScript, type DBXToolsNodeProject } from "./project.ts";
11
11
 
12
12
  /**
13
13
  * A standalone project that lives in a repo SUBDIRECTORY but is NOT a member of
package/src/scaffold.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { join } from "node:path";
5
5
  import { exec } from "@dbx-tools/core";
6
- import { repoRoot } from "./packages";
6
+ import { repoRoot } from "./packages.ts";
7
7
 
8
8
  /**
9
9
  * Re-run projen synth by executing `.projenrc.ts` with `node --import tsx` (no
package/src/tags.ts CHANGED
@@ -14,9 +14,8 @@
14
14
  * callers add their own afterward.
15
15
  */
16
16
  import type { IMixin as ConstructsMixin } from "constructs";
17
- import { DependencyType, javascript } from "projen";
18
- import { addCliBinLaunchers } from "./cli-bin";
19
- import { create } from "./mixin";
17
+ import { javascript } from "projen";
18
+ import { create } from "./mixin.ts";
20
19
  import {
21
20
  addPackageFiles,
22
21
  applyCompilerOptions,
@@ -24,9 +23,9 @@ import {
24
23
  applyIncludes,
25
24
  applyTasks,
26
25
  srcModuleExports,
27
- } from "./project";
28
- import * as projectPredicate from "./project-predicate";
29
- import { ViteConfigFile } from "./vite";
26
+ } from "./project.ts";
27
+ import * as projectPredicate from "./project-predicate.ts";
28
+ import { ViteConfigFile } from "./vite.ts";
30
29
 
31
30
  /** Node compiler options: ES2022 lib + node types, deliberately no DOM. */
32
31
  const NODE_COMPILER_OPTIONS: javascript.TypeScriptCompilerOptions = {
@@ -114,19 +113,17 @@ export const PACKAGE_TAG_MIXINS = {
114
113
  });
115
114
  }),
116
115
  cli: create(projectPredicate.hasTag("cli"), (p) => {
117
- // tsx is a RUNTIME dep, not a dev one: a CLI's bin is a `.ts` entry that needs
118
- // the tsx loader registered before it can run, and an installed consumer
119
- // (`npm i -g`) has no other source for it. Drop the baseline devDep so it isn't
120
- // declared in both blocks. The generated `.mjs` launchers are what actually
121
- // reach it - see {@link addCliBinLaunchers}.
122
- p.deps.removeDependency("tsx", DependencyType.BUILD);
123
- p.addDeps("commander@catalog:", "@clack/prompts@catalog:", "tsx@catalog:");
116
+ // No tsx: a CLI's `bin` resolves to its emitted `lib/bin/<name>.js` in the
117
+ // published tarball (see `publishConfig` in {@link applyCompiledPublish}), so
118
+ // the installed CLI is plain JavaScript Node runs directly - no loader to
119
+ // register and no launcher shim to generate. In a WORKSPACE checkout the
120
+ // manifest still points at the `.ts` entry, which Node type-strips on its own
121
+ // because the package is not under `node_modules`.
122
+ p.addDeps("commander@catalog:", "@clack/prompts@catalog:");
124
123
  p.addDevDeps("@types/node@catalog:");
125
- addCliBinLaunchers(p);
126
124
  // A CLI compiles code OUTSIDE `src/` - its root `index.ts` barrel and the
127
- // `bin/` entries - which the src-only tag default doesn't reach, so the
128
- // tsconfig is rooted at the package instead.
129
- applyCompilerOptions(p, { ...NODE_COMPILER_OPTIONS, rootDir: "." });
125
+ // `bin/` entries - which the src-only tag default doesn't reach.
126
+ applyCompilerOptions(p, NODE_COMPILER_OPTIONS);
130
127
  applyIncludes(p, "index.ts", "bin/**/*.ts");
131
128
  // A CLI's standard surface: the `.` root entry, a `./<module>` subpath per
132
129
  // top-level `src` module, and `./package.json`. Derived rather than declared
@@ -137,7 +134,7 @@ export const PACKAGE_TAG_MIXINS = {
137
134
  "./package.json": "./package.json",
138
135
  });
139
136
  // A CLI is the one shape whose entry point lives outside `src`, so its
140
- // launchers have to be added to the tarball allowlist explicitly.
137
+ // `bin/` tree has to be added to the tarball allowlist explicitly.
141
138
  addPackageFiles(p, "bin");
142
139
  }),
143
140
  server: create(projectPredicate.hasTag("server"), (p) => {
package/src/watch.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  import { isAbsolute, resolve } from "node:path";
18
18
  import { watch as fileScan } from "@dbx-tools/path";
19
19
  import { log } from "@dbx-tools/shared-core";
20
- import { isGeneratedFile, recordedRoots, repoRoot } from "./packages";
20
+ import { isGeneratedFile, recordedRoots, repoRoot } from "./packages.ts";
21
21
 
22
22
  const logger = log.logger("projen:watch");
23
23
  const DEBOUNCE_MS = 250;
package/tasks/barrels.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { sep } from "node:path";
3
- import { generateBarrels } from "../src/barrels";
3
+ import { generateBarrels } from "../src/barrels.ts";
4
4
  import { log, string } from "@dbx-tools/shared-core";
5
- import { watchLoop, watchRoots } from "../src/watch";
6
- import { recordedPackages } from "../src/packages";
5
+ import { watchLoop, watchRoots } from "../src/watch.ts";
6
+ import { recordedPackages } from "../src/packages.ts";
7
7
 
8
8
  const logger = log.logger("projen:barrels");
9
9
 
package/tasks/clean.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { relative } from "node:path";
3
- import { listGeneratedFiles, listNodeModulesDirs, removePaths } from "../src/clean";
3
+ import { listGeneratedFiles, listNodeModulesDirs, removePaths } from "../src/clean.ts";
4
4
  import { log, string } from "@dbx-tools/shared-core";
5
- import { repoRoot, toPosix } from "../src/packages";
5
+ import { repoRoot, toPosix } from "../src/packages.ts";
6
6
 
7
7
  const logger = log.logger("projen:clean");
8
8
  const yes = process.argv.includes("-y") || process.argv.includes("--yes");
package/tasks/openapi.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env -S npx tsx
2
- import { generateBarrels } from "../src/barrels";
2
+ import { generateBarrels } from "../src/barrels.ts";
3
3
  import { log, string } from "@dbx-tools/shared-core";
4
- import { generateOpenapi, isTsoaController } from "../src/openapi";
5
- import { runSynth } from "../src/scaffold";
6
- import { watchLoop, watchRoots } from "../src/watch";
4
+ import { generateOpenapi, isTsoaController } from "../src/openapi.ts";
5
+ import { runSynth } from "../src/scaffold.ts";
6
+ import { watchLoop, watchRoots } from "../src/watch.ts";
7
7
 
8
8
  const logger = log.logger("projen:openapi");
9
9
 
package/tasks/projenrc.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env -S npx tsx
2
2
  import { resolve } from "node:path";
3
3
  import { log } from "@dbx-tools/shared-core";
4
- import { runSynth } from "../src/scaffold";
5
- import { watchLoop } from "../src/watch";
6
- import { repoRoot, syncResynthPaths } from "../src/packages";
4
+ import { runSynth } from "../src/scaffold.ts";
5
+ import { watchLoop } from "../src/watch.ts";
6
+ import { repoRoot, syncResynthPaths } from "../src/packages.ts";
7
7
 
8
8
  const logger = log.logger("projen:projenrc");
9
9
 
package/tasks/sync.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { fileURLToPath } from "node:url";
3
3
  import concurrently from "concurrently";
4
4
  import { log } from "@dbx-tools/shared-core";
5
- import { runSynth } from "../src/scaffold";
5
+ import { runSynth } from "../src/scaffold.ts";
6
6
 
7
7
  const logger = log.logger("projen:sync");
8
8
 
package/src/cli-bin.ts DELETED
@@ -1,72 +0,0 @@
1
- /**
2
- * Generated `bin/<name>.mjs` launchers for a CLI package's TypeScript entries.
3
- *
4
- * A CLI's real entry is a `.ts` file, which Node cannot run on its own, so something
5
- * has to register tsx first. A `#!/usr/bin/env -S npx tsx` shebang does that only for
6
- * a WORKSPACE checkout: `npx` resolves tsx from the current working directory, and a
7
- * globally installed CLI (`npm i -g @dbx-tools/cli`) has no relationship to whatever
8
- * directory the user happens to be in - so every first invocation stalls to fetch tsx
9
- * from the network, or fails outright when offline.
10
- *
11
- * {@link CliBinLauncher} emits a tiny `.mjs` sibling that npm points its bin symlink
12
- * at. Because the launcher reaches tsx through a bare `import` specifier, NODE
13
- * resolves it relative to the launcher's own location - i.e. the CLI package's own
14
- * `node_modules` - which is exactly where the `cli` tag's runtime tsx dependency
15
- * installs it. The `.ts` entry stays the source of truth; the launcher only registers
16
- * the loader and hands off.
17
- *
18
- * {@link addCliBinLaunchers} discovers the entries by scanning the package's `bin/`
19
- * directory at synth, so a new CLI just needs its `.ts` file plus a `package.json`
20
- * bin pointing at the matching `.mjs`.
21
- */
22
- import { existsSync, readdirSync } from "node:fs";
23
- import { join } from "node:path";
24
- import { type Project, TextFile } from "projen";
25
- import { header } from "./generated";
26
-
27
- /** Directory (package-relative) holding a CLI's executable entries. */
28
- const BIN_DIR = "bin";
29
-
30
- /** Render the launcher source for a `.ts` entry sitting beside it. */
31
- function renderLauncher(entryFileName: string): string {
32
- return `#!/usr/bin/env node
33
- ${header({
34
- tool: "projen synth (cli tag)",
35
- source: `./${entryFileName}`,
36
- })}
37
- // Resolved as a bare specifier so Node looks in THIS file's package rather than the
38
- // caller's cwd - the only way a globally installed CLI finds its own tsx.
39
- import { register } from "tsx/esm/api";
40
-
41
- register();
42
- await import(new URL(${JSON.stringify(`./${entryFileName}`)}, import.meta.url).href);
43
- `;
44
- }
45
-
46
- /**
47
- * A projen-owned, read-only `.mjs` launcher that registers tsx and defers to the
48
- * `.ts` entry beside it.
49
- */
50
- export class CliBinLauncher extends TextFile {
51
- constructor(project: Project, entryFileName: string) {
52
- super(project, join(BIN_DIR, entryFileName.replace(/\.ts$/, ".mjs")), {
53
- readonly: true,
54
- executable: true,
55
- lines: renderLauncher(entryFileName).split("\n"),
56
- });
57
- }
58
- }
59
-
60
- /**
61
- * Emit a {@link CliBinLauncher} for every `.ts` entry in the package's `bin/`
62
- * directory. A package with no `bin/` directory is left alone.
63
- */
64
- export function addCliBinLaunchers(project: Project): void {
65
- const binDir = join(project.outdir, BIN_DIR);
66
- if (!existsSync(binDir)) return;
67
-
68
- for (const entry of readdirSync(binDir).sort()) {
69
- if (!entry.endsWith(".ts") || entry.endsWith(".d.ts")) continue;
70
- new CliBinLauncher(project, entry);
71
- }
72
- }
package/tasks/emit.ts DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * Append the explicit file extension Node ESM requires to every relative
3
- * specifier in a compiled tree.
4
- *
5
- * Sources are written for `moduleResolution: bundler`, so they import `"./http"`
6
- * with no extension, and `tsc` copies specifiers through untouched. Node's ESM
7
- * resolver does no extension probing, so that emitted output is unloadable -
8
- * which is the whole reason these packages used to publish source instead. The
9
- * alternative fix is to write `"./http.js"` at all 800-odd import sites and keep
10
- * writing it forever; this pass makes it a build detail instead.
11
- *
12
- * Deliberately filesystem-driven rather than clever: each specifier is resolved
13
- * against what `tsc` actually emitted, so a module that became a file and a
14
- * module that became a directory are told apart by looking, not by guessing.
15
- * Anything that does not resolve is left exactly as it was - a bare package
16
- * specifier, an asset, or a genuine mistake that should surface as itself.
17
- *
18
- * Usage: `tsx emit.ts <compiled-dir>` (relative to the package root).
19
- */
20
- import { log } from "@dbx-tools/shared-core";
21
- import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
22
- import { dirname, join, resolve } from "node:path";
23
-
24
- const logger = log.logger("projen:emit");
25
-
26
- /**
27
- * The specifier of a static import/export or a dynamic `import()`.
28
- *
29
- * Anchored on the keyword that precedes it so a relative path appearing inside a
30
- * string literal or a comment is never touched.
31
- */
32
- const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*)(["'])(\.\.?\/[^"']*)\2/g;
33
-
34
- /** Extensions that are already explicit, whether JavaScript or an asset. */
35
- const EXPLICIT = /\.(js|mjs|cjs|json|css|svg|png|jpg|jpeg|gif|webp)$/;
36
-
37
- /** Files whose specifiers matter: the emitted JavaScript and its declarations. */
38
- const EMITTED = /\.(js|mjs|cjs|d\.ts)$/;
39
-
40
- /**
41
- * What a relative specifier must become, or `undefined` to leave it alone.
42
- *
43
- * Declarations resolve against the emitted `.js` too: inside a `.d.ts`,
44
- * TypeScript maps a `"./foo.js"` specifier onto the neighbouring `foo.d.ts`, so
45
- * both file kinds want the same suffix.
46
- */
47
- function retarget(fileDir: string, specifier: string): string | undefined {
48
- if (EXPLICIT.test(specifier)) return undefined;
49
- const base = resolve(fileDir, specifier);
50
- if (existsSync(`${base}.js`)) return `${specifier}.js`;
51
- if (existsSync(join(base, "index.js"))) return `${specifier}/index.js`;
52
- return undefined;
53
- }
54
-
55
- /** Every file under `dir`, recursively. */
56
- function walk(dir: string): string[] {
57
- return readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
58
- entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)],
59
- );
60
- }
61
-
62
- /** Rewrite one file in place; returns whether anything changed. */
63
- function rewrite(file: string): boolean {
64
- const before = readFileSync(file, "utf8");
65
- const after = before.replace(SPECIFIER, (match, keyword, quote, specifier) => {
66
- const next = retarget(dirname(file), specifier as string);
67
- return next ? `${keyword}${quote}${next}${quote}` : match;
68
- });
69
- if (after === before) return false;
70
- writeFileSync(file, after);
71
- return true;
72
- }
73
-
74
- const target = resolve(process.argv[2] ?? "lib");
75
- // A package whose compile produced nothing (no emit, or a tag that replaced the
76
- // compile task) is not an error - there is simply nothing to fix up.
77
- if (existsSync(target)) {
78
- const changed = walk(target)
79
- .filter((file) => EMITTED.test(file))
80
- .filter(rewrite).length;
81
- if (changed > 0) logger.debug(`emit: resolved specifiers in ${changed} files`);
82
- }