@dbx-tools/projen 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +69 -17
  2. package/index.ts +7 -5
  3. package/package.json +9 -6
  4. package/src/_rust-project.ts +356 -0
  5. package/src/_rust-release-workflow.ts +954 -0
  6. package/src/_rust-release.ts +605 -0
  7. package/src/_task-command.ts +89 -0
  8. package/src/barrels.ts +12 -11
  9. package/src/bun-app.ts +96 -85
  10. package/src/bun-workflow.ts +60 -52
  11. package/src/engine-root.ts +5 -5
  12. package/src/openapi.ts +16 -9
  13. package/src/packages.ts +9 -9
  14. package/src/pnpm-workspace.ts +1 -1
  15. package/src/project-js.ts +19 -25
  16. package/src/project-py.ts +28 -23
  17. package/src/project-rs.ts +100 -1098
  18. package/src/projen-version.ts +9 -0
  19. package/src/publish.ts +3 -4
  20. package/src/release-catalog.ts +1 -1
  21. package/src/release.ts +212 -177
  22. package/src/scaffold.ts +1 -1
  23. package/src/uniffi.ts +7 -0
  24. package/src/watch.ts +2 -2
  25. package/src/workspace-version.ts +20 -15
  26. package/tasks/barrels.ts +4 -4
  27. package/tasks/build-rust-release.ts +68 -0
  28. package/tasks/bump.ts +3 -2
  29. package/tasks/clean.ts +3 -3
  30. package/tasks/local-publish.ts +21 -3
  31. package/tasks/openapi.ts +2 -2
  32. package/tasks/publish-npm.ts +12 -6
  33. package/tasks/publish-python.ts +2 -2
  34. package/tasks/publish-uniffi-local.ts +11 -15
  35. package/tasks/publish.ts +88 -88
  36. package/tasks/python-registry.ts +1 -1
  37. package/tasks/release-bootstrap.ts +2 -2
  38. package/tasks/release-plan.ts +1 -1
  39. package/tasks/release-please.ts +1 -1
  40. package/tasks/release-pr.ts +60 -115
  41. package/tasks/release-request.ts +8 -29
  42. package/tasks/release-summary-units.ts +2 -9
  43. package/tasks/release-summary.ts +35 -24
  44. package/tasks/release-version.ts +44 -0
  45. package/tasks/rust-release.d.ts +41 -0
  46. package/tasks/rust-release.mjs +1717 -0
  47. package/tasks/sync.ts +3 -3
  48. package/tasks/uniffi-python.js +1 -1
  49. package/tasks/uniffi.ts +7 -1
  50. package/tasks/version-check.ts +6 -5
package/src/barrels.ts CHANGED
@@ -59,7 +59,7 @@
59
59
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
60
60
  import { join, relative } from "node:path";
61
61
  import { find } from "@dbx-tools/path";
62
- import { json, string } from "@dbx-tools/shared-core";
62
+ import { json, stringUtils } from "@dbx-tools/shared-core";
63
63
  import isIdentifier from "is-identifier";
64
64
  import { header, isGenerated, makeReadonly, makeWritable, type HeaderOpts } from "./generated.ts";
65
65
  import { moduleExports, moduleStatements, type ModuleExport } from "./module-exports.ts";
@@ -125,9 +125,11 @@ const UNIFFI_BINDINGS_FILE = "bindings.ts";
125
125
  const UNIFFI_GENERATED_FILE = "_bindings.ts";
126
126
  const UNIFFI_FFI_FILE = "_bindings-ffi.ts";
127
127
 
128
- /** `pnpm-workspace` -> `pnpmWorkspace`; `local-fs` -> `localFS` (`fs` -> `FS`). */
129
- function kebabToCamel(segment: string): string {
130
- const tokens = [...string.tokenizeWithOptions({ lowerCase: true, capitalize: true }, segment)];
128
+ /** `config-tools` / `config_tools` -> `configTools`; `local-fs` -> `localFS`. */
129
+ function moduleSegmentToCamel(segment: string): string {
130
+ const tokens = [
131
+ ...stringUtils.tokenizeWithOptions({ lowerCase: true, capitalize: true }, segment),
132
+ ];
131
133
  if (tokens.length === 0) return segment;
132
134
  const [first, ...rest] = tokens;
133
135
  // Standalone acronym modules (`fs`, `ai`) stay lowercase so they match
@@ -145,14 +147,14 @@ function modulePathToNamespace(modulePath: string): string {
145
147
  const rel = modulePath.replace(/^\.\/src\//, "").replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
146
148
  const segments = rel.split("/");
147
149
  if (segments.at(-1) === "index" && segments.length > 1) segments.pop();
148
- const names = segments.map(kebabToCamel);
150
+ const names = segments.map(moduleSegmentToCamel);
149
151
  let name =
150
152
  names.length === 1
151
153
  ? names[0]!
152
154
  : names[0]! +
153
155
  names
154
156
  .slice(1)
155
- .map((s) => string.capitalize(s))
157
+ .map((s) => stringUtils.capitalize(s))
156
158
  .join("");
157
159
  if (!isIdentifier(name)) {
158
160
  name = `${name}Module`;
@@ -521,10 +523,9 @@ const WRITE_ATTEMPTS = 3;
521
523
  * and the projenrc watcher's post-synth `generateBarrels()` sweep), so any gap
522
524
  * between `makeWritable` and the write is a window in which another process's
523
525
  * `makeReadonly` lands and this write fails with
524
- * `EACCES: permission denied, open '<pkg>/index.ts'`. The gap used to span all of
525
- * the oxc parsing done for export hoisting, which made it wide enough to hit
526
- * routinely. Keeping the unlock adjacent to the write shrinks it to nothing much,
527
- * and a retry absorbs what is left.
526
+ * `EACCES: permission denied, open '<pkg>/index.ts'`. Keeping the unlock adjacent
527
+ * to the write avoids a gap during export parsing, and a retry absorbs what is
528
+ * left.
528
529
  *
529
530
  * Do NOT "simplify" this back to a single unlock-then-write: the failure is
530
531
  * timing-dependent, so it looks fine until a full-repo sweep runs against a
@@ -576,7 +577,7 @@ export function generateBarrels(
576
577
  const names = failures.map((f) => relative(projectRoot, f.dir) || f.dir);
577
578
  throw new AggregateError(
578
579
  failures.map((f) => f.err),
579
- `${string.pluralize(failures.length, "barrel")} failed: ${names.join(", ")}`,
580
+ `${stringUtils.pluralize(failures.length, "barrel")} failed: ${names.join(", ")}`,
580
581
  );
581
582
  }
582
583
  return total;
package/src/bun-app.ts CHANGED
@@ -23,6 +23,7 @@
23
23
  * `src/`), they are outside the package's `tsconfig` include, so their `Bun.*`
24
24
  * usage never trips the `app` package's DOM-only `compile`.
25
25
  */
26
+ import { stringUtils } from "@dbx-tools/shared-core";
26
27
  import { type Project, TextFile } from "projen";
27
28
 
28
29
  /** Unmanaged dev-server override module, merged over the generated options if present. */
@@ -90,34 +91,38 @@ export class BunfigFile extends TextFile {
90
91
  /** `dev.ts`: a `Bun.serve()` fullstack dev server with React HMR. */
91
92
  export class BunDevServerFile extends TextFile {
92
93
  constructor(project: Project) {
93
- super(project, "dev.ts", {
94
- marker: true,
95
- readonly: true,
96
- lines: String.raw`
97
- import { existsSync } from "node:fs";
98
- import index from "./index.html";
94
+ // prettier-ignore
95
+ const source = (
96
+ // ============================================================================
97
+ /*ts*/`
98
+ import { existsSync } from "node:fs";
99
+ import index from "./index.html";
99
100
 
100
- // Unmanaged override (${BUN_DEV_OVERRIDE}): its default export is merged over the
101
- // generated serve options - later wins - so a package tweaks the dev server
102
- // without editing this generated file.
103
- let options: Bun.ServeFunctionOptions<unknown, {}> = {
104
- port: Number(process.env.PORT ?? 5173),
105
- development: { hmr: true, console: true },
106
- routes: { "/*": index },
107
- };
101
+ // Unmanaged override (${BUN_DEV_OVERRIDE}): its default export is merged over the
102
+ // generated serve options - later wins - so a package tweaks the dev server
103
+ // without editing this generated file.
104
+ let options: Bun.ServeFunctionOptions<unknown, {}> = {
105
+ port: Number(process.env.PORT ?? 5173),
106
+ development: { hmr: true, console: true },
107
+ routes: { "/*": index },
108
+ };
108
109
 
109
- const overrideUrl = new URL("./${BUN_DEV_OVERRIDE}", import.meta.url);
110
- if (existsSync(overrideUrl)) {
111
- const mod = await import(overrideUrl.href);
112
- const override = typeof mod.default === "function" ? await mod.default() : mod.default;
113
- options = { ...options, ...override };
114
- }
110
+ const overrideUrl = new URL("./${BUN_DEV_OVERRIDE}", import.meta.url);
111
+ if (existsSync(overrideUrl)) {
112
+ const mod = await import(overrideUrl.href);
113
+ const override = typeof mod.default === "function" ? await mod.default() : mod.default;
114
+ options = { ...options, ...override };
115
+ }
115
116
 
116
- const server = Bun.serve(options);
117
- console.log("dev server running at " + server.url.href);
118
- `
119
- .trimStart()
120
- .split("\n"),
117
+ const server = Bun.serve(options);
118
+ console.log("dev server running at " + server.url.href);
119
+ `
120
+ // ============================================================================
121
+ );
122
+ super(project, "dev.ts", {
123
+ marker: true,
124
+ readonly: true,
125
+ lines: stringUtils.dedentLines(source, { trimEnd: false }),
121
126
  });
122
127
  }
123
128
  }
@@ -125,73 +130,79 @@ console.log("dev server running at " + server.url.href);
125
130
  /** `build.ts`: a `Bun.build()` production bundle (Tailwind via the plugin API). */
126
131
  export class BunBuildFile extends TextFile {
127
132
  constructor(project: Project) {
128
- super(project, "build.ts", {
129
- marker: true,
130
- readonly: true,
131
- lines: String.raw`
132
- import { existsSync } from "node:fs";
133
- import { cp, rm } from "node:fs/promises";
134
- import { basename } from "node:path";
135
- import tailwind from "bun-plugin-tailwind";
133
+ // Raw template preserves backslashes in generated regular expression literals.
134
+ // prettier-ignore
135
+ const source = (
136
+ String.raw
137
+ // ============================================================================
138
+ /*ts*/`
139
+ import { existsSync } from "node:fs";
140
+ import { cp, rm } from "node:fs/promises";
141
+ import { basename } from "node:path";
142
+ import tailwind from "bun-plugin-tailwind";
136
143
 
137
- // Unmanaged override (${BUN_BUILD_OVERRIDE}): its default export is merged over
138
- // the generated build options - later wins.
139
- let options: Bun.BuildConfig = {
140
- entrypoints: ["./index.html"],
141
- outdir: "./dist",
142
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
143
- minify: true,
144
- splitting: true,
145
- publicPath: "/",
146
- external: ["/fonts/*"],
147
- sourcemap: "none",
148
- plugins: [tailwind],
149
- };
144
+ // Unmanaged override (${BUN_BUILD_OVERRIDE}): its default export is merged over
145
+ // the generated build options - later wins.
146
+ let options: Bun.BuildConfig = {
147
+ entrypoints: ["./index.html"],
148
+ outdir: "./dist",
149
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
150
+ minify: true,
151
+ splitting: true,
152
+ publicPath: "/",
153
+ external: ["/fonts/*"],
154
+ sourcemap: "none",
155
+ plugins: [tailwind],
156
+ };
150
157
 
151
- const overrideUrl = new URL("./${BUN_BUILD_OVERRIDE}", import.meta.url);
152
- if (existsSync(overrideUrl)) {
153
- const mod = await import(overrideUrl.href);
154
- const override = typeof mod.default === "function" ? await mod.default() : mod.default;
155
- options = { ...options, ...override, plugins: [...(options.plugins ?? []), ...(override.plugins ?? [])] };
156
- }
158
+ const overrideUrl = new URL("./${BUN_BUILD_OVERRIDE}", import.meta.url);
159
+ if (existsSync(overrideUrl)) {
160
+ const mod = await import(overrideUrl.href);
161
+ const override = typeof mod.default === "function" ? await mod.default() : mod.default;
162
+ options = { ...options, ...override, plugins: [...(options.plugins ?? []), ...(override.plugins ?? [])] };
163
+ }
157
164
 
158
- const outdir = options.outdir ?? "./dist";
159
- await rm(outdir, { recursive: true, force: true });
165
+ const outdir = options.outdir ?? "./dist";
166
+ await rm(outdir, { recursive: true, force: true });
160
167
 
161
- const result = await Bun.build(options);
162
- if (!result.success) {
163
- for (const message of result.logs) console.error(message);
164
- process.exit(1);
165
- }
168
+ const result = await Bun.build(options);
169
+ if (!result.success) {
170
+ for (const message of result.logs) console.error(message);
171
+ process.exit(1);
172
+ }
166
173
 
167
- // Fix a Bun bug: with \`splitting: true\` and an HTML entrypoint, the emitted
168
- // HTML's <script src> is wired to an arbitrary chunk instead of the JS
169
- // entry-point, so the app never boots (a blank page). Rewrite the HTML script
170
- // (and stylesheet) to the real entry-point outputs. No-op when they already
171
- // match (e.g. splitting off), so it is always safe to run.
172
- const htmlOut = result.outputs.find((o) => o.path.endsWith(".html"));
173
- const entryJs = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".js"));
174
- const entryCss = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".css"));
175
- if (htmlOut && entryJs) {
176
- let html = await Bun.file(htmlOut.path).text();
177
- const jsName = basename(entryJs.path);
178
- html = html.replace(/(<script[^>]*\bsrc=")([^"]*\/)?[^"/]+\.js(")/i, "$1$2" + jsName + "$3");
179
- if (entryCss) {
180
- const cssName = basename(entryCss.path);
181
- html = html.replace(/(<link[^>]*\bhref=")([^"]*\/)?[^"/]+\.css(")/i, "$1$2" + cssName + "$3");
182
- }
183
- await Bun.write(htmlOut.path, html);
184
- }
174
+ // Fix a Bun bug: with \`splitting: true\` and an HTML entrypoint, the emitted
175
+ // HTML's <script src> is wired to an arbitrary chunk instead of the JS
176
+ // entry-point, so the app never boots (a blank page). Rewrite the HTML script
177
+ // (and stylesheet) to the real entry-point outputs. No-op when they already
178
+ // match (e.g. splitting off), so it is always safe to run.
179
+ const htmlOut = result.outputs.find((o) => o.path.endsWith(".html"));
180
+ const entryJs = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".js"));
181
+ const entryCss = result.outputs.find((o) => o.kind === "entry-point" && o.path.endsWith(".css"));
182
+ if (htmlOut && entryJs) {
183
+ let html = await Bun.file(htmlOut.path).text();
184
+ const jsName = basename(entryJs.path);
185
+ html = html.replace(/(<script[^>]*\bsrc=")([^"]*\/)?[^"/]+\.js(")/i, "$1$2" + jsName + "$3");
186
+ if (entryCss) {
187
+ const cssName = basename(entryCss.path);
188
+ html = html.replace(/(<link[^>]*\bhref=")([^"]*\/)?[^"/]+\.css(")/i, "$1$2" + cssName + "$3");
189
+ }
190
+ await Bun.write(htmlOut.path, html);
191
+ }
185
192
 
186
- const publicDir = "./public";
187
- if (existsSync(publicDir)) {
188
- await cp(publicDir, outdir, { recursive: true });
189
- }
193
+ const publicDir = "./public";
194
+ if (existsSync(publicDir)) {
195
+ await cp(publicDir, outdir, { recursive: true });
196
+ }
190
197
 
191
- console.log("built " + result.outputs.length + " files to " + options.outdir);
192
- `
193
- .trimStart()
194
- .split("\n"),
198
+ console.log("built " + result.outputs.length + " files to " + options.outdir);
199
+ `
200
+ // ============================================================================
201
+ );
202
+ super(project, "build.ts", {
203
+ marker: true,
204
+ readonly: true,
205
+ lines: stringUtils.dedentLines(source, { trimEnd: false }),
195
206
  });
196
207
  }
197
208
  }
@@ -1,4 +1,5 @@
1
1
  /** Shared Bun setup and package-cache steps for generated workflows. */
2
+ import { stringUtils } from "@dbx-tools/shared-core";
2
3
  import { TextFile, javascript } from "projen";
3
4
 
4
5
  export const BUN_VERSION = "1.3.14";
@@ -16,61 +17,68 @@ const DEFAULT_CACHE_IGNORE_PATHS = [
16
17
 
17
18
  function cacheKeyScript(extraIgnorePaths: readonly string[]): string {
18
19
  const ignorePaths = [...new Set(extraIgnorePaths)].sort();
19
- return `#!/usr/bin/env node
20
- import { createHash } from "node:crypto";
21
- import { readdirSync, readFileSync } from "node:fs";
22
- import { join, sep } from "node:path";
20
+ // prettier-ignore
21
+ const source = (
22
+ // ============================================================================
23
+ /*js*/`
24
+ #!/usr/bin/env node
25
+ import { createHash } from "node:crypto";
26
+ import { readdirSync, readFileSync } from "node:fs";
27
+ import { join, sep } from "node:path";
23
28
 
24
- const root = process.cwd();
25
- const ignoredNames = new Set(${JSON.stringify(DEFAULT_CACHE_IGNORE_PATHS)});
26
- const ignoredPaths = new Set(${JSON.stringify(ignorePaths)});
27
- const manifests = [];
28
- const walk = (directory) => {
29
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
30
- const path = join(directory, entry.name);
31
- const relativePath = path.slice(root.length + 1).split(sep).join("/");
32
- if (ignoredNames.has(entry.name) || ignoredPaths.has(relativePath)) continue;
33
- if (entry.isDirectory()) walk(path);
34
- else if (entry.name === "package.json") manifests.push(path);
35
- }
36
- };
37
- walk(root);
29
+ const root = process.cwd();
30
+ const ignoredNames = new Set(${JSON.stringify(DEFAULT_CACHE_IGNORE_PATHS)});
31
+ const ignoredPaths = new Set(${JSON.stringify(ignorePaths)});
32
+ const manifests = [];
33
+ const walk = (directory) => {
34
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
35
+ const path = join(directory, entry.name);
36
+ const relativePath = path.slice(root.length + 1).split(sep).join("/");
37
+ if (ignoredNames.has(entry.name) || ignoredPaths.has(relativePath)) continue;
38
+ if (entry.isDirectory()) walk(path);
39
+ else if (entry.name === "package.json") manifests.push(path);
40
+ }
41
+ };
42
+ walk(root);
38
43
 
39
- const dependencyFields = [
40
- "catalog",
41
- "dependencies",
42
- "devDependencies",
43
- "optionalDependencies",
44
- "overrides",
45
- "peerDependencies",
46
- "peerDependenciesMeta",
47
- "resolutions",
48
- "trustedDependencies",
49
- ];
50
- const canonical = (value) => {
51
- if (Array.isArray(value)) return value.map(canonical);
52
- if (!value || typeof value !== "object") return value;
53
- return Object.fromEntries(
54
- Object.entries(value)
55
- .sort(([left], [right]) => left.localeCompare(right))
56
- .map(([key, child]) => [key, canonical(child)]),
44
+ const dependencyFields = [
45
+ "catalog",
46
+ "dependencies",
47
+ "devDependencies",
48
+ "optionalDependencies",
49
+ "overrides",
50
+ "peerDependencies",
51
+ "peerDependenciesMeta",
52
+ "resolutions",
53
+ "trustedDependencies",
54
+ ];
55
+ const canonical = (value) => {
56
+ if (Array.isArray(value)) return value.map(canonical);
57
+ if (!value || typeof value !== "object") return value;
58
+ return Object.fromEntries(
59
+ Object.entries(value)
60
+ .sort(([left], [right]) => left.localeCompare(right))
61
+ .map(([key, child]) => [key, canonical(child)]),
62
+ );
63
+ };
64
+ const dependencies = manifests
65
+ .sort()
66
+ .map((path) => {
67
+ const manifest = JSON.parse(readFileSync(path, "utf8"));
68
+ return [
69
+ path.slice(root.length + 1),
70
+ Object.fromEntries(
71
+ dependencyFields
72
+ .filter((field) => manifest[field] !== undefined)
73
+ .map((field) => [field, canonical(manifest[field])]),
74
+ ),
75
+ ];
76
+ });
77
+ process.stdout.write(createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"));
78
+ `
79
+ // ============================================================================
57
80
  );
58
- };
59
- const dependencies = manifests
60
- .sort()
61
- .map((path) => {
62
- const manifest = JSON.parse(readFileSync(path, "utf8"));
63
- return [
64
- path.slice(root.length + 1),
65
- Object.fromEntries(
66
- dependencyFields
67
- .filter((field) => manifest[field] !== undefined)
68
- .map((field) => [field, canonical(manifest[field])]),
69
- ),
70
- ];
71
- });
72
- process.stdout.write(createHash("sha256").update(JSON.stringify(dependencies)).digest("hex"));
73
- `;
81
+ return stringUtils.dedent(source, { trimEnd: false });
74
82
  }
75
83
 
76
84
  const configured = new WeakMap<javascript.NodeProject, string>();
@@ -6,8 +6,8 @@
6
6
  */
7
7
  import { dirname } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { project } from "@dbx-tools/core";
10
- import { functionModule } from "@dbx-tools/shared-core";
9
+ import * as projectUtils from "@dbx-tools/core/project-utils";
10
+ import { functionUtils } from "@dbx-tools/shared-core";
11
11
 
12
12
  const ENGINE_PKG = "@dbx-tools/projen";
13
13
 
@@ -17,11 +17,11 @@ const ENGINE_PKG = "@dbx-tools/projen";
17
17
  * Walks up from this module with shared-core's {@link project.root} (the nearest
18
18
  * package bounded by the enclosing npm/git root), so it resolves both in-repo and
19
19
  * when installed as a dependency. Memoized with shared-core's
20
- * {@link functionModule.memoize}, which caches only a successful result - a
20
+ * {@link functionUtils.memoize}, which caches only a successful result - a
21
21
  * throw is retried on the next call.
22
22
  */
23
- export const resolvePkgRoot = functionModule.memoize((): string => {
24
- const found = project.root(dirname(fileURLToPath(import.meta.url)));
23
+ export const resolvePkgRoot = functionUtils.memoize((): string => {
24
+ const found = projectUtils.root(dirname(fileURLToPath(import.meta.url)));
25
25
  if (!found) throw new Error(`${ENGINE_PKG} package root not found`);
26
26
  return found;
27
27
  });
package/src/openapi.ts CHANGED
@@ -35,9 +35,9 @@ import {
35
35
  import { createRequire } from "node:module";
36
36
  import { join } from "node:path";
37
37
  import { promisify } from "node:util";
38
- import { bin } from "@dbx-tools/core";
38
+ import * as bin from "@dbx-tools/core/bin";
39
39
  import { find } from "@dbx-tools/path";
40
- import { log } from "@dbx-tools/shared-core";
40
+ import { log, stringUtils } from "@dbx-tools/shared-core";
41
41
  import type * as ts from "typescript";
42
42
  import { lazyRequire } from "./_lazy-require.ts";
43
43
  import { makeReadonly, makeWritable, stampGenerated } from "./generated.ts";
@@ -61,14 +61,21 @@ const SPEAKEASY_OPENAPI_VERSION = "1.24.0";
61
61
  const SPEAKEASY_OPENAPI_RELEASE_URL = `https://github.com/speakeasy-api/openapi/releases/download/v${SPEAKEASY_OPENAPI_VERSION}`;
62
62
  const execFileAsync = promisify(execFile);
63
63
 
64
- const CLIENT_SRC = `import createClient, { type ClientOptions } from "openapi-fetch";
65
- import type { paths } from "./schema";
64
+ // prettier-ignore
65
+ const CLIENT_TEMPLATE = (
66
+ // ============================================================================
67
+ /*ts*/`
68
+ import createClient, { type ClientOptions } from "openapi-fetch";
69
+ import type { paths } from "./schema";
66
70
 
67
- /** Create a typed OpenAPI client (openapi-fetch); safe to use in the browser. */
68
- export function createApiClient(options?: ClientOptions) {
69
- return createClient<paths>(options);
70
- }
71
- `;
71
+ /** Create a typed OpenAPI client (openapi-fetch); safe to use in the browser. */
72
+ export function createApiClient(options?: ClientOptions) {
73
+ return createClient<paths>(options);
74
+ }
75
+ `
76
+ // ============================================================================
77
+ );
78
+ const CLIENT_SRC = stringUtils.dedent(CLIENT_TEMPLATE, { trimEnd: false });
72
79
 
73
80
  /** True if any module file in `<pkg>/src` matches {@link TSOA_IMPORT}. */
74
81
  function hasTsoaControllers(pkg: Pick<RecordedPackage, "dir">): boolean {
package/src/packages.ts CHANGED
@@ -25,14 +25,14 @@
25
25
  */
26
26
  import { existsSync, readFileSync } from "node:fs";
27
27
  import { extname, relative, resolve, sep } from "node:path";
28
- import { project as coreProject } from "@dbx-tools/core";
28
+ import * as projectUtils from "@dbx-tools/core/project-utils";
29
29
  import { find } from "@dbx-tools/path";
30
- import { json, object, string } from "@dbx-tools/shared-core";
30
+ import { json, object, stringUtils } from "@dbx-tools/shared-core";
31
31
  import { parse } from "yaml";
32
32
 
33
33
  /** Resolve the nearest package/projenrc root at call time. */
34
34
  export function resolveRepoRoot(cwd: string = process.cwd()): string {
35
- return coreProject.root(cwd) ?? cwd;
35
+ return projectUtils.root(cwd) ?? cwd;
36
36
  }
37
37
 
38
38
  /** Current-process repository root for executable task entrypoints. */
@@ -47,11 +47,11 @@ export const DEFAULT_PACKAGE_ROOTS = ["packages"] as const;
47
47
  /**
48
48
  * A project name: the root `package.json` name, else the git remote's repo
49
49
  * name, else the root folder name. Delegates to `@dbx-tools/core`'s
50
- * {@link coreProject.name}, which is also what a consuming repo's own tooling
50
+ * {@link projectUtils.name}, which is also what a consuming repo's own tooling
51
51
  * sees, so the engine and its host agree on the name.
52
52
  */
53
53
  export function projectName(projectRoot: string = resolveRepoRoot()): string {
54
- return coreProject.name(projectRoot);
54
+ return projectUtils.name(projectRoot);
55
55
  }
56
56
 
57
57
  const MODULE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -65,7 +65,7 @@ export function toPosix(p: string): string {
65
65
 
66
66
  /**
67
67
  * Cumulative nesting tags from a package's path segments relative to its discovery
68
- * root. Each segment is kebab-cased with {@link string.toSlug} (`coolDude` ->
68
+ * root. Each segment is kebab-cased with {@link stringUtils.toSlug} (`coolDude` ->
69
69
  * `cool-dude`). The leaf folder (the package name) is excluded when there are two
70
70
  * or more segments; a lone segment tags itself.
71
71
  */
@@ -75,7 +75,7 @@ function nestingTagsFromSegments(segments: readonly string[]): string[] {
75
75
  const out: string[] = [];
76
76
  let acc = "";
77
77
  for (const segment of prefix) {
78
- const token = string.toSlug(segment);
78
+ const token = stringUtils.toSlug(segment);
79
79
  if (!token) continue;
80
80
  acc = acc ? `${acc}-${token}` : token;
81
81
  out.push(acc);
@@ -288,7 +288,7 @@ function readManifestTags(dir: string): string[] | undefined {
288
288
  */
289
289
  export function syncResynthPaths(projectRoot: string = resolveRepoRoot()): string[] {
290
290
  const paths = readDbxToolsConfig(projectRoot)?.syncResynthPaths;
291
- return Array.isArray(paths) ? string.parseList(paths.map((p) => String(p))) : [];
291
+ return Array.isArray(paths) ? stringUtils.parseList(paths.map((p) => String(p))) : [];
292
292
  }
293
293
 
294
294
  /**
@@ -322,7 +322,7 @@ export function recordedPackages(projectRoot: string = resolveRepoRoot()): Recor
322
322
  export function recordedRoots(projectRoot: string = resolveRepoRoot()): string[] {
323
323
  const configured = readDbxToolsConfig(projectRoot)?.packageRoots;
324
324
  if (Array.isArray(configured)) {
325
- const roots = string.parseList(configured.map((root) => String(root)));
325
+ const roots = stringUtils.parseList(configured.map((root) => String(root)));
326
326
  if (roots.length) return roots;
327
327
  }
328
328
  const roots = new Set<string>(DEFAULT_PACKAGE_ROOTS);
@@ -2,7 +2,7 @@
2
2
  * Inputs for `pnpm-workspace.yaml`, which projen's NATIVE
3
3
  * `javascript.PnpmWorkspaceYaml` owns.
4
4
  *
5
- * projen (>= 0.101.16) writes this file itself: every pnpm `NodeProject` gets a
5
+ * projen writes this file itself: every pnpm `NodeProject` gets a
6
6
  * `PnpmWorkspaceYaml` component typed by `PnpmWorkspaceYamlSchema`, fed from
7
7
  * `pnpmOptions.workspaceYamlOptions`. Nothing here writes a file; this only
8
8
  * supplies the options object it renders, so the whole pnpm schema (`overrides`,