@dbx-tools/projen 0.9.154 → 0.9.158

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 (49) hide show
  1. package/README.md +36 -47
  2. package/index.ts +42 -20
  3. package/package.json +8 -8
  4. package/src/_task-command.ts +7 -7
  5. package/src/appkit-workspace.ts +278 -104
  6. package/src/barrels.ts +147 -259
  7. package/src/bun-app.ts +54 -10
  8. package/src/clean.ts +6 -6
  9. package/src/engine-root.ts +4 -4
  10. package/src/mixin.ts +0 -2
  11. package/src/module-exports.ts +4 -5
  12. package/src/npm-packaging.ts +128 -0
  13. package/src/npm-release.ts +200 -0
  14. package/src/packages.ts +11 -11
  15. package/src/project-js.ts +209 -103
  16. package/src/project-predicate.ts +5 -12
  17. package/src/project-py.ts +6 -6
  18. package/src/project.ts +7 -14
  19. package/src/publish.ts +3 -3
  20. package/src/python-node-bindings.ts +16 -12
  21. package/src/python-packaging.ts +177 -0
  22. package/src/release-options.ts +2 -2
  23. package/src/release.ts +3 -3
  24. package/src/shared-core-usage.ts +17 -4
  25. package/src/tags.ts +10 -9
  26. package/src/watch.ts +7 -9
  27. package/src/workspace-lock.ts +2 -2
  28. package/src/workspace-manifest.ts +2 -2
  29. package/tasks/appkit-deploy.ts +35 -15
  30. package/tasks/barrels.ts +6 -9
  31. package/tasks/bump.ts +1 -1
  32. package/tasks/clean.ts +3 -3
  33. package/tasks/cli.ts +3 -3
  34. package/tasks/compile-workspace.ts +8 -8
  35. package/tasks/dev-watch.ts +11 -11
  36. package/tasks/local-publish.ts +6 -6
  37. package/tasks/publish-npm.ts +11 -319
  38. package/tasks/publish-python.ts +2 -180
  39. package/tasks/publish.ts +6 -11
  40. package/tasks/python-node-bindings.ts +2 -2
  41. package/tasks/python-registry.ts +6 -6
  42. package/tasks/python-sync.ts +2 -3
  43. package/tasks/release-version.ts +1 -1
  44. package/tasks/release.ts +11 -11
  45. package/tasks/sync.ts +2 -2
  46. package/tasks/test-workspace.ts +9 -9
  47. package/tasks/verify-publications.ts +9 -5
  48. package/tasks/version-check.ts +5 -5
  49. /package/{tasks → src}/python-release.ts +0 -0
package/src/project.ts CHANGED
@@ -3,18 +3,13 @@
3
3
  * dbx-tools project implementations.
4
4
  */
5
5
  import { type PathMatchInput } from "@dbx-tools/path";
6
- import { object, type OneOrMany } from "@dbx-tools/shared-core";
6
+ import { type OneOrMany, toOneOrMany } from "@dbx-tools/shared-core";
7
7
  import { type IConstruct } from "constructs";
8
8
  import { Project, type ProjectOptions } from "projen";
9
9
  import * as mixin from "./mixin.ts";
10
10
  import type { DBXToolsJavaScriptProject } from "./project-js.ts";
11
11
  import * as projectPredicate from "./project-predicate.ts";
12
12
 
13
- export * from "./project-js.ts";
14
- export * from "./project-py.ts";
15
- export * from "./python-node-bindings-component.ts";
16
- export * from "./python-node-runtime.ts";
17
-
18
13
  /** Runtime family implemented by a dbx-tools project. */
19
14
  export type DBXToolsProjectLanguage = "javascript" | "python";
20
15
 
@@ -91,29 +86,27 @@ export function applyToProjects<P extends Project>(
91
86
  if (!options?.includeRoots) predicate = predicate.and((project) => project.parent != null);
92
87
  if (options?.identifierPackageName) {
93
88
  predicate = predicate.and(
94
- projectPredicate.hasIdentifierPackageName(
95
- ...object.toOneOrMany(options.identifierPackageName),
96
- ),
89
+ projectPredicate.hasIdentifierPackageName(...toOneOrMany(options.identifierPackageName)),
97
90
  );
98
91
  }
99
92
  if (options?.name) {
100
- predicate = predicate.and(projectPredicate.hasName(...object.toOneOrMany(options.name)));
93
+ predicate = predicate.and(projectPredicate.hasName(...toOneOrMany(options.name)));
101
94
  }
102
95
  if (options?.identifierScope) {
103
96
  predicate = predicate.and(
104
- projectPredicate.hasIdentifierScope(...object.toOneOrMany(options.identifierScope)),
97
+ projectPredicate.hasIdentifierScope(...toOneOrMany(options.identifierScope)),
105
98
  );
106
99
  }
107
100
  if (options?.identifierName) {
108
101
  predicate = predicate.and(
109
- projectPredicate.hasIdentifierName(...object.toOneOrMany(options.identifierName)),
102
+ projectPredicate.hasIdentifierName(...toOneOrMany(options.identifierName)),
110
103
  );
111
104
  }
112
105
  if (options?.tags) {
113
- predicate = predicate.and(projectPredicate.hasTag(...object.toOneOrMany(options.tags)));
106
+ predicate = predicate.and(projectPredicate.hasTag(...toOneOrMany(options.tags)));
114
107
  }
115
108
  if (options?.path) {
116
- predicate = predicate.and(projectPredicate.hasPath(...object.toOneOrMany(options.path)));
109
+ predicate = predicate.and(projectPredicate.hasPath(...toOneOrMany(options.path)));
117
110
  }
118
111
  const projectMixin = mixin.create(predicate, (project) => {
119
112
  callbacks.forEach((callback) => callback(project as Project));
package/src/publish.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Two compiler options make the emitted tree loadable, both native `tsc`:
18
18
  *
19
- * - `rootDir: "."` so the package-ROOT `index.ts` barrel is compiled at all.
19
+ * - `rootDir: "."` so package-root `index*.ts` barrels are compiled at all.
20
20
  * projen's default `rootDir: "src"` puts the barrel outside the compilation,
21
21
  * which is why `lib/` would otherwise have no `index.js`.
22
22
  * - `rewriteRelativeImportExtensions`, which turns the `./x.ts` specifiers this
@@ -80,7 +80,7 @@ function compiledStem(target: string): string | undefined {
80
80
  * target that ships as-is (`./package.json`, a stylesheet, an SVG asset).
81
81
  *
82
82
  * `rootDir: "."` means the emitted tree mirrors the package layout, so the
83
- * mapping is positional: `./src/react/index.ts` -> `./lib/src/react/index.js`.
83
+ * mapping is positional: `./index.react.ts` -> `./lib/index.react.js`.
84
84
  */
85
85
  function compiledTarget(target: string): { types: string; default: string } | undefined {
86
86
  const stem = compiledStem(target);
@@ -145,7 +145,7 @@ export function applyCompiledPublish(pkg: javascript.NodeProject): void {
145
145
  if (!publishesCompiled(pkg)) return;
146
146
 
147
147
  applyCompilerOptions(pkg, COMPILED_COMPILER_OPTIONS);
148
- applyIncludes(pkg, "index.ts");
148
+ applyIncludes(pkg, "index*.ts");
149
149
  addPackageFiles(pkg, COMPILED_DIR);
150
150
 
151
151
  const config = publishConfig(pkg);
@@ -1,10 +1,11 @@
1
1
  /** Shared pyproject-backed configuration for PythonMonkey Node bindings. */
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
4
+ import { createRequire } from "node:module";
4
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
- import { find } from "@dbx-tools/path";
7
- import { object, stringUtils } from "@dbx-tools/shared-core";
7
+ import { findFilesSync } from "@dbx-tools/path";
8
+ import { isRecord, toIdentifierWithOptions } from "@dbx-tools/shared-core";
8
9
  import { parse } from "smol-toml";
9
10
  import ts from "typescript";
10
11
  import { runTaskCommand } from "./_task-command.ts";
@@ -137,7 +138,7 @@ export function resolvePythonNodeBindingModules(
137
138
  config: ResolvedPythonNodeBindings,
138
139
  ): ResolvedPythonNodeBindings {
139
140
  if (config.modules.length > 0) return config;
140
- const packageEntrypoint = Bun.resolveSync(config.entrypoint, config.projectDirectory);
141
+ const packageEntrypoint = resolveModule(config.entrypoint, config.projectDirectory);
141
142
  const modules = publicNamespaceExports(packageEntrypoint).filter(
142
143
  (module) =>
143
144
  publicFunctionExports(resolvePythonNodeBindingModule(packageEntrypoint, module)).length > 0,
@@ -147,7 +148,7 @@ export function resolvePythonNodeBindingModules(
147
148
 
148
149
  /** Map a JavaScript export or namespace to its generated Python identifier. */
149
150
  export function pythonNodeBindingFunctionName(javascriptName: string): string {
150
- const name = stringUtils.toIdentifierWithOptions({ delimiter: "_" }, javascriptName);
151
+ const name = toIdentifierWithOptions({ delimiter: "_" }, javascriptName);
151
152
  if (!isPythonNodeBindingIdentifier(name)) {
152
153
  throw new Error(
153
154
  `JavaScript export ${javascriptName} does not map to a valid Python function name`,
@@ -184,7 +185,7 @@ export function pythonNodeBindingInputFingerprint(
184
185
  for (const input of config.watchInputs) files.add(canonicalPath(resolve(root, input)));
185
186
  for (const { handlerFile } of config.functionOverrides) files.add(canonicalPath(handlerFile));
186
187
  const packageEntrypoint = canonicalPath(
187
- Bun.resolveSync(config.entrypoint, config.projectDirectory),
188
+ resolveModule(config.entrypoint, config.projectDirectory),
188
189
  );
189
190
  files.add(packageEntrypoint);
190
191
  const packageManifest = nearestPackageManifest(packageEntrypoint);
@@ -327,8 +328,7 @@ export function resolveAllPythonNodeBindings(
327
328
  /** Repository-relative Python projects that configure generated Node bindings. */
328
329
  export function pythonNodeBindingProjects(projectRoot: string): string[] {
329
330
  return [
330
- ...find
331
- .findFilesSync("**/pyproject.toml", { cwd: projectRoot })
331
+ ...findFilesSync("**/pyproject.toml", { cwd: projectRoot })
332
332
  .filter((file) => {
333
333
  const manifest = parse(readFileSync(resolve(projectRoot, file), "utf8")) as {
334
334
  tool?: { dbx_tools?: { node_bindings?: unknown } };
@@ -443,7 +443,7 @@ type BindingSourceConfig = Omit<ResolvedPythonNodeBindings, "watchInputs">;
443
443
 
444
444
  /** Transitive workspace source files reachable from configured binding modules. */
445
445
  function bindingSourceInputs(root: string, config: BindingSourceConfig): string[] {
446
- const packageEntrypoint = Bun.resolveSync(config.entrypoint, config.projectDirectory);
446
+ const packageEntrypoint = resolveModule(config.entrypoint, config.projectDirectory);
447
447
  const entrypoints =
448
448
  config.modules.length > 0
449
449
  ? config.modules.map((module) => resolvePythonNodeBindingModule(packageEntrypoint, module))
@@ -479,7 +479,7 @@ function transitiveSourceFiles(
479
479
  const dependencies = ts.preProcessFile(source, true, true).importedFiles;
480
480
  for (const dependency of dependencies) {
481
481
  try {
482
- pending.push(Bun.resolveSync(dependency.fileName, dirname(candidate)));
482
+ pending.push(resolveModule(dependency.fileName, dirname(candidate)));
483
483
  } catch {
484
484
  // Built-ins and external packages are outside the workspace source graph.
485
485
  }
@@ -501,7 +501,11 @@ export function resolvePythonNodeBindingModule(entrypoint: string, module: strin
501
501
  if (!match?.[1]) {
502
502
  throw new Error(`${entrypoint} does not export namespace ${module}`);
503
503
  }
504
- return Bun.resolveSync(match[1], dirname(entrypoint));
504
+ return resolveModule(match[1], dirname(entrypoint));
505
+ }
506
+
507
+ function resolveModule(specifier: string, directory: string): string {
508
+ return createRequire(join(directory, "package.json")).resolve(specifier);
505
509
  }
506
510
 
507
511
  function canonicalPath(path: string): string {
@@ -578,11 +582,11 @@ function validateBindings(configs: readonly ResolvedPythonNodeBindings[]): void
578
582
 
579
583
  function nodePackageName(packageName: string): string {
580
584
  const unscoped = packageName.split("/").at(-1) ?? packageName;
581
- return stringUtils.toIdentifierWithOptions({ delimiter: "_" }, unscoped);
585
+ return toIdentifierWithOptions({ delimiter: "_" }, unscoped);
582
586
  }
583
587
 
584
588
  function record(value: unknown, path: string): Record<string, unknown> {
585
- if (!object.isRecord(value)) throw new Error(`${path} must be a table`);
589
+ if (!isRecord(value)) throw new Error(`${path} must be a table`);
586
590
  return value;
587
591
  }
588
592
 
@@ -0,0 +1,177 @@
1
+ /** Side-effect-free Python workspace distribution packaging. */
2
+ import {
3
+ chmodSync,
4
+ cpSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
15
+ import { projectUtils } from "@dbx-tools/core";
16
+ import { parse, stringify } from "smol-toml";
17
+ import { runTaskCommand } from "./_task-command.ts";
18
+ import { preparePythonProjectForPublication, pythonProjectInfo } from "./python-release.ts";
19
+
20
+ interface PythonProjectFile {
21
+ readonly directory: string;
22
+ readonly name: string;
23
+ readonly version: string;
24
+ readonly path: string;
25
+ readonly private: boolean;
26
+ readonly source: string;
27
+ }
28
+
29
+ /** Python wheel and source archives accepted by Twine and package indexes. */
30
+ export function pythonDistributionPaths(directory: string): string[] {
31
+ return readdirSync(directory)
32
+ .filter((file) => file.endsWith(".whl") || file.endsWith(".tar.gz"))
33
+ .sort()
34
+ .map((file) => join(directory, file));
35
+ }
36
+
37
+ function removeTemporaryTree(root: string): void {
38
+ if (!existsSync(root)) return;
39
+ const makeWritable = (directory: string): void => {
40
+ chmodSync(directory, 0o700);
41
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
42
+ if (entry.isDirectory()) makeWritable(join(directory, entry.name));
43
+ }
44
+ };
45
+ makeWritable(root);
46
+ rmSync(root, { recursive: true, force: true });
47
+ }
48
+
49
+ function pythonProjects(root: string): PythonProjectFile[] {
50
+ const packageFiles = readdirSync(root, { withFileTypes: true })
51
+ .filter((entry) => entry.isDirectory())
52
+ .map((entry) => resolve(root, entry.name, "pyproject.toml"))
53
+ .filter(existsSync)
54
+ .sort();
55
+ const allProjects: PythonProjectFile[] = packageFiles.map((path) => {
56
+ const source = readFileSync(path, "utf8");
57
+ const info = pythonProjectInfo(source, { parse });
58
+ return {
59
+ directory: basename(resolve(path, "..")),
60
+ name: info.name,
61
+ version: info.version,
62
+ path,
63
+ private: info.private,
64
+ source,
65
+ };
66
+ });
67
+ return allProjects;
68
+ }
69
+
70
+ /** Build and upload every selected public Python workspace distribution. */
71
+ export function publishPythonProjects(options: {
72
+ readonly dryRun?: boolean;
73
+ readonly indexUrl: string;
74
+ readonly publishUrl: string;
75
+ readonly root: string;
76
+ readonly version: string;
77
+ }): void {
78
+ const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
79
+ try {
80
+ buildPythonProjectsInternal({ ...options, output }, true);
81
+ runTaskCommand(
82
+ process.cwd(),
83
+ "uvx",
84
+ [
85
+ "--from",
86
+ "devpi-client",
87
+ "devpi",
88
+ "upload",
89
+ "--index",
90
+ options.publishUrl,
91
+ "--from-dir",
92
+ ...(options.dryRun ? ["--dry-run"] : []),
93
+ output,
94
+ ],
95
+ { env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl } },
96
+ );
97
+ } finally {
98
+ removeTemporaryTree(output);
99
+ }
100
+ }
101
+
102
+ /** Build the exact Python release distributions without publishing them. */
103
+ export function buildPythonProjects(options: {
104
+ readonly allowEmpty?: boolean;
105
+ readonly output: string;
106
+ readonly packages?: readonly string[];
107
+ /** Place each package's artifacts in its directory beneath output. */
108
+ readonly packageDirectories?: boolean;
109
+ readonly root: string;
110
+ readonly version: string;
111
+ }): void {
112
+ buildPythonProjectsInternal(options, false);
113
+ }
114
+
115
+ function buildPythonProjectsInternal(
116
+ options: Parameters<typeof buildPythonProjects>[0],
117
+ allowOutputOutsideWorkspace: boolean,
118
+ ): void {
119
+ const root = resolve(options.root);
120
+ const output = resolve(options.output);
121
+ const workspace = projectUtils.root(root) ?? dirname(root);
122
+ const outputPath = relative(workspace, output);
123
+ if (
124
+ !allowOutputOutsideWorkspace &&
125
+ (!outputPath || outputPath.startsWith("..") || isAbsolute(outputPath))
126
+ ) {
127
+ throw new Error("Python release output must be a non-root directory inside the workspace");
128
+ }
129
+ rmSync(output, { recursive: true, force: true });
130
+ const allProjects = pythonProjects(root);
131
+ const requested = new Set(options.packages ?? []);
132
+ const missing = [...requested].filter(
133
+ (directory) => !allProjects.some((project) => project.directory === directory),
134
+ );
135
+ if (missing.length > 0) throw new Error(`Unknown Python packages: ${missing.join(", ")}`);
136
+ const projects = allProjects.filter(
137
+ (project) => !project.private && (requested.size === 0 || requested.has(project.directory)),
138
+ );
139
+ if (projects.length === 0 && options.allowEmpty) {
140
+ mkdirSync(output, { recursive: true });
141
+ return;
142
+ }
143
+ if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
144
+
145
+ const temporaryRoot = mkdtempSync(join(tmpdir(), "projen-python-release-"));
146
+ try {
147
+ mkdirSync(output, { recursive: true });
148
+ for (const project of projects) {
149
+ const packageRoot = join(temporaryRoot, project.directory);
150
+ cpSync(dirname(project.path), packageRoot, { recursive: true });
151
+ const manifestPath = join(packageRoot, "pyproject.toml");
152
+ chmodSync(manifestPath, 0o644);
153
+ writeFileSync(
154
+ manifestPath,
155
+ preparePythonProjectForPublication(project.source, {
156
+ packages: allProjects,
157
+ toml: { parse, stringify },
158
+ version: options.version,
159
+ }),
160
+ );
161
+ const packageOutput = options.packageDirectories ? join(output, project.directory) : output;
162
+ runTaskCommand(root, "uv", ["build", "--out-dir", packageOutput, packageRoot]);
163
+ if (pythonDistributionPaths(packageOutput).length === 0) {
164
+ throw new Error(`No Python distributions found for ${project.name}`);
165
+ }
166
+ }
167
+ const distributions = options.packageDirectories
168
+ ? projects.flatMap((project) => pythonDistributionPaths(join(output, project.directory)))
169
+ : pythonDistributionPaths(output);
170
+ if (distributions.length === 0) {
171
+ throw new Error(`No Python distributions found in ${output}`);
172
+ }
173
+ runTaskCommand(process.cwd(), "uvx", ["twine", "check", ...distributions]);
174
+ } finally {
175
+ removeTemporaryTree(temporaryRoot);
176
+ }
177
+ }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module
9
9
  */
10
- import { json } from "@dbx-tools/shared-core";
10
+ import { parseRecord } from "@dbx-tools/shared-core";
11
11
  import { z } from "zod";
12
12
 
13
13
  /** Publication scopes accepted by the release task; auto preserves normal publication. */
@@ -70,7 +70,7 @@ export function parseReleaseTagAnnotation(annotation: string): ReleaseStepSelect
70
70
  const selections = annotation.split("\n").filter((line) => line.startsWith(ANNOTATION_MARKER));
71
71
  if (selections.length === 0) return releaseStepSelection();
72
72
  if (selections.length !== 1) throw new Error("Release tag contains multiple step selections");
73
- const value = json.parseRecord(selections[0]!.slice(ANNOTATION_MARKER.length));
73
+ const value = parseRecord(selections[0]!.slice(ANNOTATION_MARKER.length));
74
74
  const parsed = ReleaseStepSelectionSchema.safeParse(value);
75
75
  if (!parsed.success) throw new Error("Invalid release tag step selection");
76
76
  return parsed.data;
package/src/release.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * jobs download them. `DBXToolsRelease` therefore remains separate while native
11
11
  * workflow and project primitives own the YAML model and setup steps.
12
12
  */
13
- import { stringUtils } from "@dbx-tools/shared-core";
13
+ import { dedent } from "@dbx-tools/shared-core";
14
14
  import { Component, github } from "projen";
15
15
  import { GithubWorkflow } from "projen/lib/github";
16
16
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
@@ -216,7 +216,7 @@ export function refreshPublicationVerification(project: DBXToolsJavaScriptProjec
216
216
  VERIFY_PYPI: hasPypi ? "${{ needs.build-release.outputs.pypi }}" : "false",
217
217
  },
218
218
  // prettier-ignore
219
- run: stringUtils.dedent(
219
+ run: dedent(
220
220
  // ============================================================================
221
221
  /*bash*/`
222
222
  args=(--version "$RELEASE_VERSION")
@@ -282,7 +282,7 @@ function releaseBuildJob(
282
282
  RELEASE_TAG: "${{ github.ref_name }}",
283
283
  },
284
284
  // prettier-ignore
285
- run: stringUtils.dedent(
285
+ run: dedent(
286
286
  // ============================================================================
287
287
  /*bash*/`
288
288
  case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import { readFileSync } from "node:fs";
9
9
  import { join, relative, resolve } from "node:path";
10
- import { find } from "@dbx-tools/path";
10
+ import { findFiles } from "@dbx-tools/path";
11
11
  import * as core from "@dbx-tools/shared-core";
12
- import { PACKAGE_IDENTIFIER, object } from "@dbx-tools/shared-core";
12
+ import { PACKAGE_IDENTIFIER, sequence } from "@dbx-tools/shared-core";
13
13
  import { PYTHON_GENERATED_PACKAGE } from "./generated.ts";
14
14
  import { readPackageManifest, recordedPackages, repoRoot } from "./packages.ts";
15
15
 
@@ -33,6 +33,7 @@ export const SHARED_CORE_BINDING_EXPORTS = [
33
33
 
34
34
  const SOURCE_PATTERN = "**/*.{ts,tsx,js,mjs}";
35
35
  const NAMESPACE_EXPORT = /^export \* as (\w+) from "\.\/src\/([^"]+)\.ts";$/gm;
36
+ const ROOT_NAMED_IMPORT = /import\s*\{([\s\S]*?)\}\s*from\s*["']@dbx-tools\/shared-core["']/g;
36
37
  const LOCAL_OWNED_HELPER =
37
38
  /(?:export\s+)?(?:async\s+)?function(?:\s*\*)?\s+(isRecord|sleep|escapeRegExp|trimToNull|trimToUndefined|trimToEmpty)\s*\(|(?:export\s+)?const\s+(isRecord|sleep|escapeRegExp|trimToNull|trimToUndefined|trimToEmpty)\s*=/g;
38
39
 
@@ -70,7 +71,7 @@ async function sourceFiles(root: string, includeGenerated: boolean): Promise<str
70
71
  ];
71
72
  if (!includeGenerated) ignore.push(`**/${PYTHON_GENERATED_PACKAGE}/**`);
72
73
  const files: string[] = [];
73
- for await (const file of find.findFiles(SOURCE_PATTERN, { cwd: root, ignore })) {
74
+ for await (const file of findFiles(SOURCE_PATTERN, { cwd: root, ignore })) {
74
75
  files.push(resolve(root, file));
75
76
  }
76
77
  return files.sort();
@@ -109,6 +110,18 @@ function mentionsExport(
109
110
  moduleFile: string,
110
111
  ): boolean {
111
112
  if (source.includes(`${namespace}.${name}`)) return true;
113
+ ROOT_NAMED_IMPORT.lastIndex = 0;
114
+ for (const match of source.matchAll(ROOT_NAMED_IMPORT)) {
115
+ const specifiers = match[1]!.split(",").map((specifier) => specifier.trim());
116
+ if (
117
+ specifiers.some((specifier) => {
118
+ if (!specifier || specifier.startsWith("type ")) return false;
119
+ return specifier.split(/\s+as\s+/u, 1)[0] === name;
120
+ })
121
+ ) {
122
+ return true;
123
+ }
124
+ }
112
125
  const imported =
113
126
  source.includes(`from "@dbx-tools/shared-core/${moduleFile}"`) ||
114
127
  source.includes(`from "./${moduleFile}.ts"`) ||
@@ -173,7 +186,7 @@ export async function duplicatedOwnedHelpers(root: string = repoRoot): Promise<s
173
186
 
174
187
  /** Render a compact usage table for `shared-core:usage`. */
175
188
  export function formatSharedCoreUsage(usage: readonly SharedCoreExportUsage[]): string {
176
- const counts = object.sequence(usage).group({
189
+ const counts = sequence(usage).group({
177
190
  production: (row) => row.kind === "production",
178
191
  binding: (row) => row.kind === "binding",
179
192
  internal: (row) => row.kind === "internal",
package/src/tags.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * across the subtree; the root applies built-in tag mixins during construction and
14
14
  * callers add their own afterward.
15
15
  */
16
+ import { relative } from "node:path";
16
17
  import type { IMixin as ConstructsMixin } from "constructs";
17
18
  import { DependencyType, javascript } from "projen";
18
19
  import { BunBuildFile, BunDevServerFile, BunfigFile } from "./bun-app.ts";
@@ -23,7 +24,7 @@ import {
23
24
  applyCompilerOptions,
24
25
  applyExports,
25
26
  applyIncludes,
26
- srcModuleExports,
27
+ srcFolderExports,
27
28
  } from "./project-js.ts";
28
29
  import * as projectPredicate from "./project-predicate.ts";
29
30
 
@@ -83,11 +84,13 @@ export const PACKAGE_TAG_MIXINS = {
83
84
  applyCompilerOptions(p, {
84
85
  target: "ES2022",
85
86
  lib: [...DOM_LIB],
87
+ noEmit: true,
88
+ rootDir: relative(p.outdir, p.root.outdir).replaceAll("\\", "/") || ".",
86
89
  });
87
- // UI libraries are subpath-only. Their React entrypoint, optional stylesheet,
88
- // and any other public source modules are discovered from the source tree.
90
+ // UI libraries expose their package barrel, folder barrels, and public CSS.
89
91
  applyExports(p, {
90
- ...srcModuleExports(p),
92
+ ".": "./index.ts",
93
+ ...srcFolderExports(p),
91
94
  "./package.json": "./package.json",
92
95
  });
93
96
  }),
@@ -149,13 +152,11 @@ export const PACKAGE_TAG_MIXINS = {
149
152
  // A CLI compiles code OUTSIDE `src/` - its root `index.ts` barrel and the
150
153
  // `bin/` entries - which the src-only tag default doesn't reach.
151
154
  applyCompilerOptions(p, NODE_COMPILER_OPTIONS);
152
- applyIncludes(p, "index.ts", "bin/**/*.ts");
153
- // A CLI's standard surface: the `.` root entry, a `./<module>` subpath per
154
- // top-level `src` module, and `./package.json`. Derived rather than declared
155
- // so no CLI has to hand-list its own modules.
155
+ applyIncludes(p, "index*.ts", "bin/**/*.ts");
156
+ // A CLI's standard surface: the root barrel, folder barrels, and package metadata.
156
157
  applyExports(p, {
157
158
  ".": "./index.ts",
158
- ...srcModuleExports(p),
159
+ ...srcFolderExports(p),
159
160
  "./package.json": "./package.json",
160
161
  });
161
162
  // A CLI is the one shape whose entry point lives outside `src`, so its
package/src/watch.ts CHANGED
@@ -16,8 +16,8 @@
16
16
  * is thin glue.
17
17
  */
18
18
  import { isAbsolute, resolve } from "node:path";
19
- import { watch as pathWatch } from "@dbx-tools/path";
20
- import { asyncUtils, errorUtils, log } from "@dbx-tools/shared-core";
19
+ import { watchFiles } from "@dbx-tools/path";
20
+ import { log, sleep, toError } from "@dbx-tools/shared-core";
21
21
  import { isGeneratedFile, recordedRoots, repoRoot } from "./packages.ts";
22
22
  import { withWorkspaceMutationLock } from "./workspace-lock.ts";
23
23
 
@@ -49,9 +49,7 @@ const CLOSE_GRACE_MS = 2_000;
49
49
  const FATAL_WATCH_ERROR_CODES = new Set(["EMFILE", "ENFILE", "ENOSPC"]);
50
50
 
51
51
  /** node-path's built-in ignore-group toggles (`{ dot, temp, test, lock, defaults }`). */
52
- export type IgnoreGroupOptions = NonNullable<
53
- Parameters<typeof pathWatch.watchFiles>[1]
54
- >["ignoreOptions"];
52
+ export type IgnoreGroupOptions = NonNullable<Parameters<typeof watchFiles>[1]>["ignoreOptions"];
55
53
 
56
54
  /** The package roots (absolute), where every watchable source file lives. */
57
55
  export function watchRoots(): string[] {
@@ -119,7 +117,7 @@ export async function watchLoop(
119
117
  });
120
118
  }
121
119
  } catch (err) {
122
- logger.error(`${tag} cycle failed`, { error: errorUtils.toError(err) });
120
+ logger.error(`${tag} cycle failed`, { error: toError(err) });
123
121
  } finally {
124
122
  running = false;
125
123
  if (rerun) {
@@ -129,7 +127,7 @@ export async function watchLoop(
129
127
  }
130
128
  }
131
129
 
132
- const watcher = await pathWatch.watchFiles(paths, {
130
+ const watcher = await watchFiles(paths, {
133
131
  cwd: repoRoot,
134
132
  ignoreInitial: true,
135
133
  ignore: (path) => ignoredPath(path),
@@ -146,7 +144,7 @@ export async function watchLoop(
146
144
  if (closing) return;
147
145
  closing = true;
148
146
  clearTimeout(timer);
149
- await Promise.race([watcher.close().catch(() => {}), asyncUtils.sleep(CLOSE_GRACE_MS)]);
147
+ await Promise.race([watcher.close().catch(() => {}), sleep(CLOSE_GRACE_MS)]);
150
148
  process.exit(code);
151
149
  }
152
150
 
@@ -156,7 +154,7 @@ export async function watchLoop(
156
154
  timer = setTimeout(() => void flush(), DEBOUNCE_MS);
157
155
  });
158
156
  watcher.on("error", (err) => {
159
- logger.error(`${tag} watcher error`, { error: errorUtils.toError(err) });
157
+ logger.error(`${tag} watcher error`, { error: toError(err) });
160
158
  const code = (err as NodeJS.ErrnoException | undefined)?.code;
161
159
  if (code === undefined || !FATAL_WATCH_ERROR_CODES.has(code)) return;
162
160
  logger.error(`${tag}: ${code} leaves the watcher blind - exiting so it is restarted`);
@@ -1,6 +1,6 @@
1
1
  /** Cross-process coordination for generated workspace mutations. */
2
2
  import { resolve } from "node:path";
3
- import * as fileLock from "@dbx-tools/core/file-lock";
3
+ import { withFileLock } from "@dbx-tools/core";
4
4
  import { log } from "@dbx-tools/shared-core";
5
5
 
6
6
  const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
@@ -36,7 +36,7 @@ export async function withWorkspaceMutationLock<T>(
36
36
  }
37
37
  let waitLogTimer: ReturnType<typeof setInterval> | undefined;
38
38
  try {
39
- return await fileLock.withFileLock(
39
+ return await withFileLock(
40
40
  [MUTATION_LOCK_SCOPE, repository],
41
41
  async () => {
42
42
  if (check && !(await check())) {
@@ -17,7 +17,7 @@ import {
17
17
  } from "node:fs";
18
18
  import { tmpdir } from "node:os";
19
19
  import { dirname, join, resolve } from "node:path";
20
- import * as exec from "@dbx-tools/core/exec";
20
+ import { spawnSync } from "@dbx-tools/core";
21
21
 
22
22
  const DEPENDENCY_FIELDS = [
23
23
  "dependencies",
@@ -102,7 +102,7 @@ export async function withStagedWorkspaceManifests<T>(
102
102
  ): Promise<T> {
103
103
  const stage = mkdtempSync(join(tmpdir(), "dbx-tools-app-deploy-"));
104
104
  try {
105
- const listed = exec.spawnSync(
105
+ const listed = spawnSync(
106
106
  "git",
107
107
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
108
108
  {