@dbx-tools/projen 0.9.153 → 0.9.157

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 +44 -52
  2. package/index.ts +43 -21
  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 +8 -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-context.ts +4 -0
  23. package/src/release-options.ts +2 -2
  24. package/src/release.ts +133 -13
  25. package/src/shared-core-usage.ts +17 -4
  26. package/src/tags.ts +10 -9
  27. package/src/watch.ts +7 -9
  28. package/src/workspace-lock.ts +2 -2
  29. package/src/workspace-manifest.ts +2 -2
  30. package/tasks/appkit-deploy.ts +35 -15
  31. package/tasks/barrels.ts +6 -9
  32. package/tasks/bump.ts +1 -1
  33. package/tasks/clean.ts +3 -3
  34. package/tasks/cli.ts +3 -3
  35. package/tasks/compile-workspace.ts +8 -8
  36. package/tasks/dev-watch.ts +11 -11
  37. package/tasks/local-publish.ts +6 -6
  38. package/tasks/publish-npm.ts +11 -317
  39. package/tasks/publish-python.ts +2 -180
  40. package/tasks/publish.ts +6 -11
  41. package/tasks/python-node-bindings.ts +2 -2
  42. package/tasks/python-registry.ts +6 -6
  43. package/tasks/python-sync.ts +2 -3
  44. package/tasks/release-version.ts +1 -1
  45. package/tasks/release.ts +11 -11
  46. package/tasks/sync.ts +2 -2
  47. package/tasks/test-workspace.ts +9 -9
  48. package/tasks/verify-publications.ts +273 -0
  49. package/tasks/version-check.ts +5 -5
  50. /package/{tasks → src}/python-release.ts +0 -0
package/src/project-js.ts CHANGED
@@ -6,17 +6,30 @@
6
6
  * {@link DBXToolsNodeProject} (monorepo root) and {@link DBXToolsTypeScriptProject}
7
7
  * (a package, or a standalone compiling root) both implement {@link DBXToolsJavaScriptProject}.
8
8
  */
9
- import { existsSync, readdirSync } from "node:fs";
9
+ import { existsSync } from "node:fs";
10
10
  import { dirname, join, relative, resolve } from "node:path";
11
- import * as projectUtils from "@dbx-tools/core/project-utils";
12
- import { ignore, match } from "@dbx-tools/path";
13
- import { object, stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
11
+ import { repositoryUrl } from "@dbx-tools/core";
12
+ import { ignore, findFilesSync, toPathMatcher } from "@dbx-tools/path";
13
+ import {
14
+ type OneOrMany,
15
+ isRecord,
16
+ sequence,
17
+ toSlug,
18
+ tokenize,
19
+ trimToNull,
20
+ dedent,
21
+ } from "@dbx-tools/shared-core";
14
22
  import { type IConstruct } from "constructs";
15
23
  import { Component, LogLevel, Project, type Task, TextFile, javascript, typescript } from "projen";
16
24
  import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
17
25
  import type { ReleaseProjectOptions } from "projen/lib/release";
18
- import { AppKitWorkspace, type AppKitWorkspaceMode } from "./appkit-workspace.ts";
19
- import { generateBarrels, isGeneratedBarrel } from "./barrels.ts";
26
+ import {
27
+ AppKitWorkspace,
28
+ isAppKitApplication,
29
+ type AppKitBundleOptions,
30
+ type AppKitWorkspaceMode,
31
+ } from "./appkit-workspace.ts";
32
+ import { folderBarrelFile, generateBarrels } from "./barrels.ts";
20
33
 
21
34
  import { BUN_APP_OVERRIDES, RootBunfigFile } from "./bun-app.ts";
22
35
  import { BUN_VERSION, BUN_VERSION_CHECK_SCRIPT, MINIMUM_BUN_VERSION } from "./bun-workflow.ts";
@@ -27,7 +40,7 @@ import { resolvePkgRoot } from "./engine-root.ts";
27
40
  import * as mixin from "./mixin.ts";
28
41
  import {
29
42
  DEFAULT_PACKAGE_ROOTS,
30
- type DiscoveredPackage,
43
+ DiscoveredPackage,
31
44
  projectName,
32
45
  readPackageManifest,
33
46
  scanPackages,
@@ -113,10 +126,10 @@ export class PackageIdentifier {
113
126
  */
114
127
  static of(...names: OneOrMany<string>): PackageIdentifier {
115
128
  const segments = names.flatMap((part) => part.split("/")).filter(Boolean);
116
- const scope = segments.length ? stringUtils.toSlug(segments[0]!) : "";
129
+ const scope = segments.length ? toSlug(segments[0]!) : "";
117
130
  const nameParts = [
118
131
  scope,
119
- ...segments.slice(1).flatMap((segment) => [...stringUtils.tokenize(segment)]),
132
+ ...segments.slice(1).flatMap((segment) => [...tokenize(segment)]),
120
133
  ].filter(Boolean);
121
134
  if (!nameParts.length) throw new Error(`Invalid name: ${names.join(", ")}`);
122
135
  if (nameParts.length === 1) return new PackageIdentifier(undefined, nameParts[0]!);
@@ -144,11 +157,11 @@ function repositoryOptions(
144
157
  const existingRepositoryUrl =
145
158
  typeof existingRepository === "string"
146
159
  ? existingRepository
147
- : object.isRecord(existingRepository)
148
- ? stringUtils.trimToNull(existingRepository.url)
160
+ : isRecord(existingRepository)
161
+ ? trimToNull(existingRepository.url)
149
162
  : undefined;
150
163
  const repository =
151
- options.repository ?? projectUtils.repositoryUrl(workspaceRoot, "npm") ?? existingRepositoryUrl;
164
+ options.repository ?? repositoryUrl(workspaceRoot, "npm") ?? existingRepositoryUrl;
152
165
  const projectRoot = options.parent
153
166
  ? resolve(options.parent.outdir, options.outdir ?? ".")
154
167
  : resolve(options.outdir ?? process.cwd());
@@ -239,7 +252,7 @@ function dependencyName(spec: string): string {
239
252
  /** Add an optional peer and make it available while developing the package. */
240
253
  export function addOptionalPeer(pkg: javascript.NodeProject, spec: string): void {
241
254
  const name = dependencyName(spec);
242
- const current = object.isRecord(pkg.package.manifest.peerDependenciesMeta)
255
+ const current = isRecord(pkg.package.manifest.peerDependenciesMeta)
243
256
  ? pkg.package.manifest.peerDependenciesMeta
244
257
  : {};
245
258
  pkg.addPeerDeps(spec);
@@ -256,8 +269,8 @@ export function projectRepositoryUrl(project: javascript.NodeProject): string |
256
269
  const configured =
257
270
  typeof repository === "string"
258
271
  ? repository
259
- : object.isRecord(repository)
260
- ? (stringUtils.trimToNull(repository.url) ?? undefined)
272
+ : isRecord(repository)
273
+ ? (trimToNull(repository.url) ?? undefined)
261
274
  : undefined;
262
275
  return configured?.replace(/^git\+/, "").replace(/\.git$/, "");
263
276
  }
@@ -285,47 +298,45 @@ export function projectReleaseBranch(project: DBXToolsJavaScriptProject): string
285
298
  */
286
299
  export function addPackageFiles(pkg: javascript.NodeProject, ...entries: string[]): void {
287
300
  const current = (pkg.package.manifest.files ?? []) as string[];
288
- pkg.package.addField("files", [...object.sequence(current, entries).distinct()]);
301
+ pkg.package.addField("files", [...sequence(current, entries).distinct()]);
289
302
  }
290
303
 
291
- /**
292
- * The automatic subpath map for a package's public top-level TypeScript and CSS
293
- * files plus directory entrypoints, skipping `_`-prefixed private modules and
294
- * declaration files.
295
- *
296
- * This widens no API surface: the root `index.ts` barrel already re-exports every
297
- * non-`_` module, so those names are public through `.` either way - the subpaths
298
- * just add a narrower import path. Deriving the map is what lets a tag carry the
299
- * whole export layout, instead of each package hand-listing its own modules.
300
- * The wildcard resolves generated nested barrels at every depth while exact
301
- * exports continue to win for hand-authored entrypoints and assets.
302
- */
303
- export function srcModuleExports(pkg: javascript.NodeProject): Record<string, string> {
304
+ /** Generate folder-only TypeScript subpaths plus public CSS asset subpaths. */
305
+ export function srcFolderExports(
306
+ pkg: javascript.NodeProject,
307
+ additionalModules: readonly string[] = [],
308
+ ): Record<string, string> {
304
309
  const srcDir = join(pkg.outdir, "src");
305
310
  if (!existsSync(srcDir)) return {};
306
311
 
312
+ const discovered = [...findFilesSync("**/*", { cwd: srcDir })].map(toPosix);
313
+ const modules = new Set([
314
+ ...discovered.filter(
315
+ (file) =>
316
+ /\.(?:ts|tsx)$/.test(file) &&
317
+ !file.endsWith(".d.ts") &&
318
+ !/(^|\/)_/.test(file) &&
319
+ !/\.(?:test|spec)\./.test(file) &&
320
+ !/(^|\/)index\.(?:ts|tsx)$/.test(file),
321
+ ),
322
+ ...additionalModules
323
+ .map((file) => toPosix(file).replace(/^src\//, ""))
324
+ .filter((file) => /\.(?:ts|tsx)$/.test(file) && !/(^|\/)_/.test(file)),
325
+ ]);
326
+ const directories = new Set<string>();
327
+ for (const module of modules) {
328
+ const directory = dirname(module);
329
+ if (directory !== ".") directories.add(directory);
330
+ }
331
+
307
332
  const exports: Record<string, string> = {};
308
- for (const entry of readdirSync(srcDir, { withFileTypes: true }).sort((left, right) =>
309
- left.name.localeCompare(right.name),
310
- )) {
311
- if (entry.name.startsWith("_")) continue;
312
- if (entry.isFile() && /\.(?:ts|tsx)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
313
- exports[`./${entry.name.replace(/\.tsx?$/, "")}`] = `./src/${entry.name}`;
314
- continue;
315
- }
316
- if (entry.isFile() && entry.name.endsWith(".css")) {
317
- exports[`./${entry.name}`] = `./src/${entry.name}`;
318
- continue;
319
- }
320
- if (!entry.isDirectory()) continue;
321
- const index = ["index.ts", "index.tsx"].find((file) =>
322
- existsSync(join(srcDir, entry.name, file)),
323
- );
324
- if (index && !isGeneratedBarrel(join(srcDir, entry.name, index))) {
325
- exports[`./${entry.name}`] = `./src/${entry.name}/${index}`;
326
- }
333
+ for (const directory of [...directories].sort((left, right) => left.localeCompare(right))) {
334
+ exports[`./${directory}`] = `./${folderBarrelFile(directory)}`;
327
335
  }
328
- return { ...exports, "./*": "./src/*/index.ts" };
336
+ for (const file of discovered.filter((file) => file.endsWith(".css") && !/(^|\/)_/.test(file))) {
337
+ exports[`./${file}`] = `./src/${file}`;
338
+ }
339
+ return exports;
329
340
  }
330
341
 
331
342
  /**
@@ -377,8 +388,6 @@ const PRETTIER_SETTINGS: javascript.PrettierSettings = {
377
388
  endOfLine: javascript.EndOfLine.LF,
378
389
  };
379
390
 
380
- export { PROJEN_VERSION } from "./projen-version.ts";
381
-
382
391
  /** SPDX license shared by generated JavaScript and Python packages. */
383
392
  export const LICENSE = "Apache-2.0";
384
393
 
@@ -520,10 +529,8 @@ const DEV_DEPS_ROOT: string[] = [
520
529
 
521
530
  function configureRootBunRuntime(project: DBXToolsJavaScriptProject): void {
522
531
  new TextFile(project, BUN_VERSION_CHECK_SCRIPT, {
523
- lines: stringUtils
524
- .dedent(
525
- // ============================================================================
526
- /*ts*/ `
532
+ lines: dedent(// ============================================================================
533
+ /*ts*/ `
527
534
  /** Minimum Bun version accepted by this workspace. */
528
535
  const MINIMUM_VERSION = ${JSON.stringify(MINIMUM_BUN_VERSION)};
529
536
 
@@ -550,10 +557,8 @@ function configureRootBunRuntime(project: DBXToolsJavaScriptProject): void {
550
557
  \`Bun \${MINIMUM_VERSION} or newer is required; running \${Bun.version}. Use "bun run bun" to select the workspace-local Bun binary.\`,
551
558
  );
552
559
  }
553
- `,
554
- // ============================================================================
555
- )
556
- .split("\n"),
560
+ `).split("\n"),
561
+ // ============================================================================
557
562
  });
558
563
  const check = `bun ${BUN_VERSION_CHECK_SCRIPT}`;
559
564
  project.package.installTask.prependExec(check);
@@ -638,6 +643,13 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
638
643
  * `auto`; `true` also validates the required package roles and entry files.
639
644
  */
640
645
  readonly appkit?: AppKitWorkspaceMode;
646
+ /**
647
+ * One Bun/AppKit application root. Defaults to `packages/app` when that
648
+ * directory exists. Set false to disable application-root discovery.
649
+ */
650
+ readonly appRoot?: string | false;
651
+ /** Minimal generated bundle settings, or false for an application-owned bundle. */
652
+ readonly appBundle?: false | AppKitBundleOptions;
641
653
  /**
642
654
  * Extra repo-root paths that trigger a full re-synth during `sync --watch`
643
655
  * (alongside `.projenrc.ts`). Repo-relative, e.g. `".example.projenrc.ts"`.
@@ -877,22 +889,22 @@ export class DBXToolsTypeScriptProject
877
889
  // instead of failing, so authoring a React component would otherwise need
878
890
  // per-package tsconfig config to be compiled at all.
879
891
  this.tsconfig?.addInclude("src/**/*.tsx");
892
+ this.tsconfig?.addInclude("index.*.ts");
880
893
  this.dbxToolsConfig = new DBXToolsConfig(this, options);
881
894
  // Source-first entry: point the package at its package-ROOT `index.ts` barrel
882
895
  // so packages resolve each other's `@scope/pkg` imports to source.
883
896
  this.package.addField("type", "module");
884
897
  this.package.addField("exports", {
885
898
  ".": "./index.ts",
886
- ...srcModuleExports(this),
899
+ ...srcFolderExports(this),
887
900
  "./package.json": "./package.json",
888
901
  });
889
902
  this.package.addVersion(readWorkspaceVersion(this.root.outdir));
890
- addPackageFiles(this, "index.ts", "src");
891
- if (existsSync(join(this.outdir, "exports.ts"))) addPackageFiles(this, "exports.ts");
903
+ addPackageFiles(this, "index*.ts", "src");
892
904
  // `bun test` intercepts `node:test` (the suites keep using node:test) and
893
905
  // runs it with bun's own fast runner. The native no-tests option is portable
894
906
  // across Windows and Unix and lets packages without tests remain a no-op.
895
- this.testTask.exec("bun test test --pass-with-no-tests");
907
+ this.testTask.exec(`bun test ${this.testdir} --pass-with-no-tests`);
896
908
  initProject(this, options);
897
909
  }
898
910
 
@@ -945,19 +957,11 @@ class GeneratedSource extends Component {
945
957
  }
946
958
  if (modules.length === 0) continue;
947
959
  const current = (subproject.package.manifest.exports ?? {}) as Record<string, unknown>;
948
- const sourceExports = Object.fromEntries(
949
- modules.map((module) => [`./${module.slice("src/".length, -".ts".length)}`, `./${module}`]),
950
- );
951
- const nestedExport = current["./*"];
960
+ const { "./package.json": packageJson, ...withoutPackageJson } = current;
952
961
  subproject.package.addField("exports", {
953
- ...Object.fromEntries(
954
- Object.entries(current).filter(
955
- ([subpath]) => subpath !== "./*" && subpath !== "./package.json",
956
- ),
957
- ),
958
- ...sourceExports,
959
- ...(nestedExport ? { "./*": nestedExport } : {}),
960
- ...(current["./package.json"] ? { "./package.json": current["./package.json"] } : {}),
962
+ ...withoutPackageJson,
963
+ ...srcFolderExports(subproject, modules),
964
+ ...(packageJson ? { "./package.json": packageJson } : {}),
961
965
  });
962
966
  }
963
967
  }
@@ -1058,6 +1062,62 @@ class EslintIgnoreCodegen extends Component {
1058
1062
  }
1059
1063
  }
1060
1064
 
1065
+ /** Require package implementation files to import their own modules relatively. */
1066
+ class EslintWorkspaceImports extends Component {
1067
+ private readonly configured = new Set<string>();
1068
+
1069
+ public override preSynthesize(): void {
1070
+ const eslint = javascript.Eslint.of(this.project);
1071
+ if (!eslint) return;
1072
+ const root = this.project as DBXToolsNodeProject | DBXToolsTypeScriptProject;
1073
+ const rootAbs = resolve(root.outdir);
1074
+ const configure = (directory: string, packageName: string): void => {
1075
+ const rel = toPosix(relative(rootAbs, directory));
1076
+ const key = `${rel}:${packageName}`;
1077
+ if (this.configured.has(key)) return;
1078
+ this.configured.add(key);
1079
+ eslint.addOverride({
1080
+ files: [`${rel ? `${rel}/` : ""}**/*.{ts,tsx}`],
1081
+ rules: {
1082
+ "no-restricted-imports": [
1083
+ "error",
1084
+ {
1085
+ patterns: [
1086
+ {
1087
+ group: [packageName, `${packageName}/**`],
1088
+ message: "Use relative imports within this package.",
1089
+ },
1090
+ ],
1091
+ },
1092
+ ],
1093
+ },
1094
+ });
1095
+ };
1096
+
1097
+ const members = root.node
1098
+ .findAll()
1099
+ .filter(
1100
+ (candidate): candidate is DBXToolsNodeProject | DBXToolsTypeScriptProject =>
1101
+ candidate !== root &&
1102
+ (candidate instanceof DBXToolsNodeProject ||
1103
+ candidate instanceof DBXToolsTypeScriptProject),
1104
+ );
1105
+ for (const member of members) {
1106
+ configure(member.outdir, member.package.packageName);
1107
+ }
1108
+ if (members.length === 0 && root.extraWorkspaceMembers.length === 0) {
1109
+ configure(root.outdir, root.package.packageName);
1110
+ }
1111
+ for (const member of root.extraWorkspaceMembers) {
1112
+ const directory = resolve(rootAbs, member);
1113
+ const packageName = readPackageManifest(directory)?.name;
1114
+ if (typeof packageName === "string" && packageName.trim()) {
1115
+ configure(directory, packageName);
1116
+ }
1117
+ }
1118
+ }
1119
+ }
1120
+
1061
1121
  /**
1062
1122
  * Keep generated package barrels and codegen modules out of root formatting.
1063
1123
  *
@@ -1074,15 +1134,14 @@ class PrettierIgnoreGenerated extends Component {
1074
1134
  for (const sub of this.project.subprojects) {
1075
1135
  if (!(sub instanceof DBXToolsTypeScriptProject)) continue;
1076
1136
  const rel = toPosix(relative(rootAbs, sub.outdir));
1077
- prettier.addIgnorePattern(`${rel}/index.ts`);
1078
- prettier.addIgnorePattern(`${rel}/src/**/index.ts`);
1137
+ prettier.addIgnorePattern(`${rel}/index*.ts`);
1079
1138
  for (const module of codegenModulePaths(sub.dbxToolsConfig.codegenInputs)) {
1080
1139
  prettier.addIgnorePattern(`${rel}/${module}`);
1081
1140
  }
1082
1141
  }
1083
1142
  const extraMembers = (this.project as DBXToolsNodeProject).extraWorkspaceMembers ?? [];
1084
1143
  for (const member of extraMembers) {
1085
- prettier.addIgnorePattern(`${member}/index.ts`);
1144
+ prettier.addIgnorePattern(`${member}/index*.ts`);
1086
1145
  }
1087
1146
  }
1088
1147
  }
@@ -1094,7 +1153,7 @@ const DEFAULT_OMIT_RELATIVE_PREFIX = ["node"];
1094
1153
  function resolveOmitRelativePrefix(option: OneOrMany<string> | undefined): string[] {
1095
1154
  const raw = option === undefined ? DEFAULT_OMIT_RELATIVE_PREFIX : option;
1096
1155
  const list = Array.isArray(raw) ? raw : [raw];
1097
- return list.map((segment) => stringUtils.toSlug(segment)).filter(Boolean);
1156
+ return list.map((segment) => toSlug(segment)).filter(Boolean);
1098
1157
  }
1099
1158
 
1100
1159
  /**
@@ -1104,7 +1163,7 @@ function resolveOmitRelativePrefix(option: OneOrMany<string> | undefined): strin
1104
1163
  */
1105
1164
  function packageNameFor(scope: string, relPath: string, omitPrefixes: string[]): string {
1106
1165
  const segments = relPath.split("/").filter(Boolean);
1107
- if (segments.length > 1 && omitPrefixes.includes(stringUtils.toSlug(segments[0]!))) {
1166
+ if (segments.length > 1 && omitPrefixes.includes(toSlug(segments[0]!))) {
1108
1167
  segments.shift();
1109
1168
  }
1110
1169
  return PackageIdentifier.of(scope, segments.join("/")).packageName;
@@ -1145,14 +1204,14 @@ function engineSelfDependency(project: javascript.NodeProject): string | undefin
1145
1204
  const enginePkgJson = join(resolvePkgRoot(), "package.json");
1146
1205
  if (!toPosix(enginePkgJson).includes("/node_modules/")) return undefined;
1147
1206
  const engine = readPackageManifest(dirname(enginePkgJson));
1148
- const name = stringUtils.trimToNull(engine?.name);
1207
+ const name = trimToNull(engine?.name);
1149
1208
  if (!name) return undefined;
1150
- const version = stringUtils.trimToNull(engine?.version);
1209
+ const version = trimToNull(engine?.version);
1151
1210
 
1152
1211
  // No existing consumer manifest (or no entry) falls through to a computed pin.
1153
1212
  const consumer = readPackageManifest(resolve(project.outdir));
1154
1213
  const dependencyOf = (field: unknown): string | undefined =>
1155
- object.isRecord(field) ? (stringUtils.trimToNull(field[name]) ?? undefined) : undefined;
1214
+ isRecord(field) ? (trimToNull(field[name]) ?? undefined) : undefined;
1156
1215
  const existing = dependencyOf(consumer?.devDependencies) ?? dependencyOf(consumer?.dependencies);
1157
1216
  if (existing) return `${name}@${existing}`;
1158
1217
  return `${name}@^${version}`;
@@ -1174,7 +1233,7 @@ function tagPathMatches(key: string, p: DiscoveredPackage): boolean {
1174
1233
  return true;
1175
1234
  }
1176
1235
  // Otherwise treat the key as a glob against the same targets.
1177
- const isMatch = match.toPathMatcher(key);
1236
+ const isMatch = toPathMatcher(key);
1178
1237
  return isMatch(p.relPath) || isMatch(p.memberPath) || p.tagCandidates.some((c) => isMatch(c));
1179
1238
  }
1180
1239
 
@@ -1278,6 +1337,12 @@ function initProject(
1278
1337
  const roots =
1279
1338
  options.packageRoots ??
1280
1339
  (existsSync(resolve(project.outdir, "packages/js")) ? ["packages/js"] : DEFAULT_PACKAGE_ROOTS);
1340
+ const rootAbs = resolve(project.outdir);
1341
+ const configuredAppRoot =
1342
+ options.appRoot === false
1343
+ ? undefined
1344
+ : (options.appRoot ??
1345
+ (existsSync(resolve(rootAbs, "packages/app")) ? "packages/app" : undefined));
1281
1346
  project.dbxToolsConfig.packageRoots = [...roots];
1282
1347
  if (options.syncResynthPaths?.length) {
1283
1348
  project.dbxToolsConfig.syncResynthPaths = [...options.syncResynthPaths];
@@ -1317,11 +1382,11 @@ function initProject(
1317
1382
  ".npmrc",
1318
1383
  );
1319
1384
  for (const root of roots) {
1320
- project.annotateGenerated(`/${root}/**/index.ts`);
1385
+ project.annotateGenerated(`/${root}/**/index*.ts`);
1321
1386
  project.annotateGenerated(`/${root}/**/src/generated/**`);
1322
1387
  }
1323
1388
  for (const member of project.extraWorkspaceMembers) {
1324
- project.annotateGenerated(`/${member}/index.ts`);
1389
+ project.annotateGenerated(`/${member}/index*.ts`);
1325
1390
  }
1326
1391
 
1327
1392
  // ESLint lives ONLY on the root and lints every package. `projectService` resolves
@@ -1350,11 +1415,15 @@ function initProject(
1350
1415
  // Generated read-only outputs (barrels, app scripts, and codegen). ESLint
1351
1416
  // --fix cannot rewrite files owned by the barrel generator, codegen, or projen.
1352
1417
  for (const root of roots) {
1353
- eslint.addIgnorePattern(`${root}/**/index.ts`);
1418
+ eslint.addIgnorePattern(`${root}/**/index*.ts`);
1354
1419
  }
1355
1420
  eslint.addIgnorePattern("**/src/generated/**");
1356
1421
  for (const member of project.extraWorkspaceMembers) {
1357
- eslint.addIgnorePattern(`${member}/index.ts`);
1422
+ eslint.addIgnorePattern(`${member}/index*.ts`);
1423
+ }
1424
+ if (configuredAppRoot) {
1425
+ eslint.addIgnorePattern(`${configuredAppRoot}/index*.ts`);
1426
+ eslint.addIgnorePattern(`${configuredAppRoot}/client/vite.config.ts`);
1358
1427
  }
1359
1428
  // The generated bun app scripts + unmanaged overrides live at the package root,
1360
1429
  // outside any `src/**` tsconfig include, so the type-aware parser cannot resolve
@@ -1367,6 +1436,7 @@ function initProject(
1367
1436
  // Codegen inputs may be added by mixins after construction; ignore their
1368
1437
  // generated modules once typed package configuration is final.
1369
1438
  new EslintIgnoreCodegen(project);
1439
+ new EslintWorkspaceImports(project);
1370
1440
  eslint.addRules({
1371
1441
  "import/no-relative-packages": "error",
1372
1442
  // Monorepo tooling legitimately uses devDeps (typescript, tsx, projen) in src.
@@ -1396,6 +1466,9 @@ function initProject(
1396
1466
  "tsconfig.json",
1397
1467
  ...roots.map((r) => `${r}/**/tsconfig.json`),
1398
1468
  ...project.extraWorkspaceMembers.map((member) => `${member}/tsconfig.json`),
1469
+ ...(configuredAppRoot
1470
+ ? [`${configuredAppRoot}/tsconfig.client.json`, `${configuredAppRoot}/tsconfig.server.json`]
1471
+ : []),
1399
1472
  ];
1400
1473
  }
1401
1474
 
@@ -1410,7 +1483,6 @@ function initProject(
1410
1483
  };
1411
1484
 
1412
1485
  // Already-attached subprojects, keyed by repo-relative member path.
1413
- const rootAbs = resolve(project.outdir);
1414
1486
  const existing = new Map<string, DBXToolsJavaScriptProject>();
1415
1487
  for (const sub of project.subprojects) {
1416
1488
  if (sub instanceof DBXToolsNodeProject || sub instanceof DBXToolsTypeScriptProject) {
@@ -1418,12 +1490,52 @@ function initProject(
1418
1490
  }
1419
1491
  }
1420
1492
 
1493
+ let application: DBXToolsTypeScriptProject | undefined;
1494
+ let applicationPath: string | undefined;
1495
+ if (configuredAppRoot) {
1496
+ const appPath = toPosix(relative(rootAbs, resolve(rootAbs, configuredAppRoot)));
1497
+ if (!appPath || appPath.startsWith("../")) {
1498
+ throw new Error(`appRoot must resolve beneath the workspace root: ${configuredAppRoot}`);
1499
+ }
1500
+ applicationPath = appPath;
1501
+ const segments = appPath.split("/").filter(Boolean);
1502
+ const discovered = new DiscoveredPackage(rootAbs, segments[0]!, segments.slice(1));
1503
+ const found = existing.get(appPath);
1504
+ if (found) {
1505
+ if (!(found instanceof DBXToolsTypeScriptProject)) {
1506
+ throw new Error(`appRoot must be a TypeScript project: ${appPath}`);
1507
+ }
1508
+ application = found;
1509
+ } else {
1510
+ const defaults = {
1511
+ name: packageNameFor(project.scope, discovered.relPath, omitPrefixes),
1512
+ ...options.packageDefaults,
1513
+ publishable: false,
1514
+ } satisfies DiscoveredTypeScriptPackageOptions;
1515
+ const packageOptions = options.resolvePackageOptions?.(discovered, defaults) ?? defaults;
1516
+ application = new DBXToolsTypeScriptProject({
1517
+ parent: project,
1518
+ outdir: appPath,
1519
+ ...packageOptions,
1520
+ publishable: false,
1521
+ tags: [],
1522
+ });
1523
+ existing.set(appPath, application);
1524
+ }
1525
+ }
1526
+
1421
1527
  // Discover + append a child per src-bearing folder. A root encapsulating an
1422
1528
  // already-attached project doesn't re-create it, it just unions the tags in. The
1423
1529
  // agnostic floor is set in the child's constructor; per-tag deps/tsconfig come from
1424
1530
  // the PACKAGE_TAG_MIXINS applied across the subtree below.
1425
1531
  for (const p of scanPackages(rootAbs, roots)) {
1426
- const tags = [...object.sequence(p.tagCandidates, resolveTags(p, tagPaths)).distinct()];
1532
+ if (
1533
+ applicationPath &&
1534
+ (p.memberPath === applicationPath || p.memberPath.startsWith(`${applicationPath}/`))
1535
+ ) {
1536
+ continue;
1537
+ }
1538
+ const tags = [...sequence(p.tagCandidates, resolveTags(p, tagPaths)).distinct()];
1427
1539
  const found = existing.get(p.memberPath);
1428
1540
  if (found) {
1429
1541
  found.dbxToolsConfig.tags.push(...tags);
@@ -1444,7 +1556,7 @@ function initProject(
1444
1556
  }
1445
1557
 
1446
1558
  // The root project may itself carry tags (via a `""`/`"."` tag-path key).
1447
- const rootTags = [...object.sequence(tagPaths[""], tagPaths["."]).distinct()];
1559
+ const rootTags = [...sequence(tagPaths[""], tagPaths["."]).distinct()];
1448
1560
  if (rootTags.length) project.dbxToolsConfig.tags.push(...rootTags);
1449
1561
 
1450
1562
  // Apply per-tag mixins across the whole subtree now that every child exists
@@ -1455,19 +1567,13 @@ function initProject(
1455
1567
  }
1456
1568
  const appkitMode = options.appkit ?? "auto";
1457
1569
  if (appkitMode !== false) {
1458
- const projects = [project, ...project.subprojects].filter(
1459
- (candidate): candidate is DBXToolsNodeProject | DBXToolsTypeScriptProject =>
1460
- candidate instanceof DBXToolsNodeProject || candidate instanceof DBXToolsTypeScriptProject,
1461
- );
1462
- const apps = projects.filter((candidate) => candidate.dbxToolsConfig.tags.includes("app"));
1463
- const servers = projects.filter((candidate) =>
1464
- candidate.dbxToolsConfig.tags.includes("server"),
1465
- );
1466
- const enabled = appkitMode === true || (apps.length > 0 && servers.length > 0);
1467
- if (enabled) {
1570
+ if (appkitMode === true && !application) {
1571
+ throw new Error("appkit: true requires an application root");
1572
+ }
1573
+ if (application && (appkitMode === true || isAppKitApplication(application))) {
1468
1574
  new AppKitWorkspace(project, {
1469
- apps,
1470
- servers,
1575
+ application,
1576
+ bundle: options.appBundle,
1471
1577
  validate: appkitMode === true,
1472
1578
  });
1473
1579
  }
@@ -3,14 +3,8 @@
3
3
  * package identity, tags, and repository path.
4
4
  */
5
5
  import { relative } from "node:path";
6
- import { match, PathMatchInput, PathMatchPredicate } from "@dbx-tools/path";
7
- import {
8
- object,
9
- predicate,
10
- Sequence,
11
- type OneOrMany,
12
- type Predicate,
13
- } from "@dbx-tools/shared-core";
6
+ import { PathMatchInput, PathMatchPredicate, toPathMatcher } from "@dbx-tools/path";
7
+ import { Sequence, type OneOrMany, type Predicate, sequence, create } from "@dbx-tools/shared-core";
14
8
  import { IConstruct } from "constructs";
15
9
  import { Project } from "projen";
16
10
  import { toPosix } from "./packages.ts";
@@ -28,7 +22,7 @@ import type { DBXToolsProject } from "./project.ts";
28
22
  * silently reject that construct before the install problem can be diagnosed.
29
23
  */
30
24
  export function isProject(): Predicate<IConstruct, Project> {
31
- return predicate.create((c: IConstruct): c is Project => Project.isProject(c));
25
+ return create((c: IConstruct): c is Project => Project.isProject(c));
32
26
  }
33
27
 
34
28
  /** Guard: the construct implements the language-agnostic {@link DBXToolsProject} contract. */
@@ -53,9 +47,8 @@ export function isDBXToolsJavaScriptProject(): Predicate<IConstruct, DBXToolsJav
53
47
  * the resulting predicate.
54
48
  */
55
49
  function projectMatchers(...inputs: OneOrMany<PathMatchInput>): Sequence<PathMatchPredicate> {
56
- return object
57
- .sequence(inputs)
58
- .map((input) => match.toPathMatcher(input))
50
+ return sequence(inputs)
51
+ .map((input) => toPathMatcher(input))
59
52
  .cache();
60
53
  }
61
54
 
package/src/project-py.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /** Reusable uv workspace generation for Python packages hosted in a projen tree. */
2
2
  import { existsSync, readdirSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
- import * as projectUtils from "@dbx-tools/core/project-utils";
5
- import { stringUtils, type OneOrMany } from "@dbx-tools/shared-core";
4
+ import { projectUtils } from "@dbx-tools/core";
5
+ import { type OneOrMany, toSlug, dedent } from "@dbx-tools/shared-core";
6
6
  import { Component, License, TextFile, type Project, github, javascript, python } from "projen";
7
7
  import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
8
8
  import { header, PYTHON_GENERATED_PACKAGE } from "./generated.ts";
@@ -19,6 +19,7 @@ import {
19
19
  PythonNodeRuntime,
20
20
  } from "./python-node-runtime.ts";
21
21
  import {
22
+ refreshPublicationVerification,
22
23
  refreshReleaseDocsDependencies,
23
24
  releaseCondition,
24
25
  releaseBuildSteps,
@@ -349,8 +350,8 @@ export class DBXToolsPythonWorkspace extends Component {
349
350
  constructor(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions) {
350
351
  super(project);
351
352
  const scope = isDBXToolsJavaScriptProject()(project)
352
- ? stringUtils.toSlug(project.scope)
353
- : stringUtils.toSlug(project.name).replace(/-root$/, "");
353
+ ? toSlug(project.scope)
354
+ : toSlug(project.name).replace(/-root$/, "");
354
355
  const repositoryUrl =
355
356
  options.repository?.url ??
356
357
  projectRepositoryUrl(project) ??
@@ -378,7 +379,7 @@ export class DBXToolsPythonWorkspace extends Component {
378
379
  directory,
379
380
  description: `${scope} ${directory} Python package.`,
380
381
  ...pkg,
381
- name: pkg?.name ?? `${scope}-${stringUtils.toSlug(directory)}`,
382
+ name: pkg?.name ?? `${scope}-${toSlug(directory)}`,
382
383
  module: pkg?.module ?? pythonModuleName(scope, directory),
383
384
  };
384
385
  });
@@ -705,6 +706,7 @@ export class DBXToolsPythonWorkspace extends Component {
705
706
  ],
706
707
  });
707
708
  }
709
+ refreshPublicationVerification(project);
708
710
  refreshReleaseDocsDependencies(project);
709
711
  }
710
712
 
@@ -800,7 +802,7 @@ export class DBXToolsPythonWorkspace extends Component {
800
802
  marker: true,
801
803
  readonly: true,
802
804
  // prettier-ignore
803
- lines: stringUtils.dedent(
805
+ lines: dedent(
804
806
  // ============================================================================
805
807
  /*js*/`
806
808
  #!/usr/bin/env bun