@dereekb/dbx-cli 13.43.0 → 14.0.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/validate/index.js CHANGED
@@ -2909,6 +2909,96 @@ var RULE_CATALOG = [
2909
2909
  whenItApplies: "When a key on `<app>SystemStateStoredDataConverterMap` is neither a local `<NAME>_SYSTEM_STATE_TYPE` constant nor an imported identifier.",
2910
2910
  whenItDoesNotApply: "Keys imported from upstream packages \u2014 those are detected via `importedIdentifiers` and skipped.",
2911
2911
  canonicalFix: "Either declare the missing constant locally or import it from the upstream package that owns it."
2912
+ },
2913
+ {
2914
+ code: "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_MISSING",
2915
+ source: "dbx_workspace_validate",
2916
+ severity: "error",
2917
+ title: "A configuration's file-replacement or esbuild-config path does not exist on disk",
2918
+ whatItFlags: "A configuration's file-replacement or esbuild-config path does not exist on disk.",
2919
+ whenItApplies: "When a `fileReplacements` `replace` / `with` path, or a configuration's `esbuildConfig` path, names a file that is absent from the working tree.",
2920
+ whenItDoesNotApply: "Generated files produced by an earlier target in the same pipeline \u2014 declare them as that target's `outputs` so the dependency is explicit, or the reference is genuinely broken.",
2921
+ canonicalFix: "Create the missing file or correct the path. An Angular build fails loudly on a missing replacement, but an esbuild `additionalProperties: true` schema accepts a bad path silently."
2922
+ },
2923
+ {
2924
+ code: "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_UNREFERENCED",
2925
+ source: "dbx_workspace_validate",
2926
+ severity: "warning",
2927
+ title: "An `environment.<name>.ts` file exists but no build configuration references it",
2928
+ whatItFlags: "An `environment.<name>.ts` file exists but no build configuration references it.",
2929
+ whenItApplies: "When a file matching `environment.*.ts` sits in a project's `src/environments/` directory and appears in no configuration's `fileReplacements`, and no `esbuildConfig` file references it.",
2930
+ whenItDoesNotApply: "The base `environment.ts` (always the replacement source, never a target), and environment files imported directly by application code rather than swapped in by the build.",
2931
+ canonicalFix: "Add the configuration that consumes it, or delete the file. An unreachable environment file reads as a working deploy lane that does not exist \u2014 the file is edited, reviewed, and never compiled."
2932
+ },
2933
+ {
2934
+ code: "WORKSPACE_DEPLOY_HOSTING_TARGET_MISSING",
2935
+ source: "dbx_workspace_validate",
2936
+ severity: "error",
2937
+ title: "A deploy command targets a firebase hosting target that `firebase.json` does not declare",
2938
+ whatItFlags: "A deploy command targets a firebase hosting target that `firebase.json` does not declare.",
2939
+ whenItApplies: "When a deploy command runs `firebase deploy --only hosting:<target>` and `<target>` matches no `target` (or `site`) entry under `hosting` in `firebase.json`.",
2940
+ whenItDoesNotApply: "Workspaces that resolve hosting targets through `.firebaserc` target aliases the scanner does not read \u2014 verify manually if this misfires.",
2941
+ canonicalFix: "Add the hosting entry to `firebase.json`, or point the command at a declared target. `firebase deploy` fails on an unknown hosting target, so this breaks the lane outright."
2942
+ },
2943
+ {
2944
+ code: "WORKSPACE_DEPLOY_LANE_ASYMMETRIC",
2945
+ source: "dbx_workspace_validate",
2946
+ severity: "warning",
2947
+ title: "A deploy lane is wired on one side of the app/api pair but not the other",
2948
+ whatItFlags: "A deploy lane is wired on one side of the app/api pair but not the other.",
2949
+ whenItApplies: "When one project in the workspace declares `ci-deploy-<lane>` and a sibling project that declares at least one other `ci-deploy-*` lane does not declare this one.",
2950
+ whenItDoesNotApply: "Projects deliberately deployed on their own cadence, and workspaces with a single deployable project. Suppress by giving the sibling the lane, or accept the warning.",
2951
+ canonicalFix: "Add the missing `ci-deploy-<lane>` target to the sibling project. A lane present on the API but not the web app (or vice versa) deploys half an application and leaves the two halves on different builds."
2952
+ },
2953
+ {
2954
+ code: "WORKSPACE_DEPLOY_LANE_NO_ENVIRONMENT_SELECTION",
2955
+ source: "dbx_workspace_validate",
2956
+ severity: "error",
2957
+ title: "A deploy lane builds a configuration that selects no environment file",
2958
+ whatItFlags: "A deploy lane builds a configuration that selects no environment file.",
2959
+ whenItApplies: "When a `ci-deploy-<lane>` target builds a configuration that provides no environment-selection mechanism for its executor \u2014 no `fileReplacements` for an Angular `application` build, or no lane-specific `esbuildConfig` for an `@nx/esbuild:esbuild` build.",
2960
+ whenItDoesNotApply: "Non-deploy configurations such as `development`, which are expected to compile the base `environment.ts`. Only configurations reachable from a `ci-deploy-*` target are checked.",
2961
+ canonicalFix: "Add the `fileReplacements` entry (or the lane's `esbuildConfig`) to the configuration. Without it the lane deploys the unreplaced `environment.ts` \u2014 localhost URLs, dev credentials, and any developer-only function map \u2014 to a live project."
2962
+ },
2963
+ {
2964
+ code: "WORKSPACE_TARGET_OUTPUT_ESCAPES_ROOT",
2965
+ source: "dbx_workspace_validate",
2966
+ severity: "error",
2967
+ title: "A target's `outputs` entry resolves outside the workspace root",
2968
+ whatItFlags: "A target's `outputs` entry resolves outside the workspace root.",
2969
+ whenItApplies: 'When an `outputs` entry \u2014 after `{options.<name>}` substitution from the target\'s own options \u2014 resolves to a path above the workspace root. The common shape is `outputs: ["{options.reportsDirectory}"]` with a `../../`-relative option value.',
2970
+ whenItDoesNotApply: "Entries anchored by `{workspaceRoot}` or `{projectRoot}`, and entries whose `{options.<name>}` token has no resolvable value (skipped rather than guessed).",
2971
+ canonicalFix: 'Anchor the entry explicitly, e.g. `"{workspaceRoot}/coverage/{projectRoot}"`. Nx refuses to cache an output it cannot place inside the workspace and will report the task as flaky instead of failing loudly.'
2972
+ },
2973
+ {
2974
+ code: "WORKSPACE_TARGET_REF_CONFIGURATION_MISSING",
2975
+ source: "dbx_workspace_validate",
2976
+ severity: "error",
2977
+ title: "An `nx run <project>:<target>:<configuration>` reference names a configuration the target does not declare",
2978
+ whatItFlags: "An `nx run <project>:<target>:<configuration>` reference names a configuration the target does not declare.",
2979
+ whenItApplies: "Whenever a reference carries a configuration segment and the target's `configurations` map has no such key.",
2980
+ whenItDoesNotApply: "Targets that declare no `configurations` at all \u2014 Nx tolerates a configuration argument there, so it is not reported.",
2981
+ canonicalFix: "Rename the reference to a declared configuration (a `prod` / `production` mismatch is the usual cause), or add the configuration to the target. Nx does NOT error on an unknown configuration: `create-task-graph.js` silently substitutes `defaultConfiguration`, so a mismatch here ships whatever that default builds \u2014 and ships the base options with no configuration merged at all when no default is set."
2982
+ },
2983
+ {
2984
+ code: "WORKSPACE_TARGET_REF_PROJECT_MISSING",
2985
+ source: "dbx_workspace_validate",
2986
+ severity: "error",
2987
+ title: "An `nx run <project>:...` reference names a project that does not exist in the workspace",
2988
+ whatItFlags: "An `nx run <project>:...` reference names a project that does not exist in the workspace.",
2989
+ whenItApplies: "When a target reference in a `project.json` command, CI config, shell script, or npm script names a project with no matching entry in the workspace project graph.",
2990
+ whenItDoesNotApply: "References built from shell variables or CI interpolation the scanner cannot resolve \u2014 those are skipped rather than reported.",
2991
+ canonicalFix: "Create the project, or update the reference to the project's real name. A renamed or deleted project leaves these references behind and they only fail at deploy time."
2992
+ },
2993
+ {
2994
+ code: "WORKSPACE_TARGET_REF_TARGET_MISSING",
2995
+ source: "dbx_workspace_validate",
2996
+ severity: "error",
2997
+ title: "An `nx run <project>:<target>` reference names a target the project does not declare",
2998
+ whatItFlags: "An `nx run <project>:<target>` reference names a target the project does not declare.",
2999
+ whenItApplies: "When the referenced project exists but has no target by that name (after `nx.json` `targetDefaults` are accounted for).",
3000
+ whenItDoesNotApply: "Targets contributed by an inferred-target plugin the scanner does not evaluate \u2014 declare the plugin's targets explicitly if this misfires.",
3001
+ canonicalFix: "Add the target to the project, or point the reference at an existing one. This is the single most common way a CI lane silently stops doing anything."
2912
3002
  }
2913
3003
  ];
2914
3004
 
@@ -2996,22 +3086,22 @@ function formatSeeAlso(ref) {
2996
3086
  // packages/dbx-cli/validate/src/lib/_core/validate-input.ts
2997
3087
  import { glob as fsGlob, readFile, stat } from "node:fs/promises";
2998
3088
  import { resolve, sep } from "node:path";
2999
- function ensurePathInsideCwd(relative12, cwd) {
3089
+ function ensurePathInsideCwd(relative13, cwd) {
3000
3090
  const cwdPrefix = cwd.endsWith(sep) ? cwd : cwd + sep;
3001
- const absolute = resolve(cwd, relative12);
3091
+ const absolute = resolve(cwd, relative13);
3002
3092
  if (!absolute.startsWith(cwdPrefix) && absolute !== cwd) {
3003
- throw new Error(`Path \`${relative12}\` resolves outside the server cwd and is not allowed.`);
3093
+ throw new Error(`Path \`${relative13}\` resolves outside the server cwd and is not allowed.`);
3004
3094
  }
3005
3095
  }
3006
3096
  async function resolveFolderPaths(config) {
3007
3097
  const { paths, glob, cwd } = config;
3008
3098
  const collected = [];
3009
3099
  const seen = /* @__PURE__ */ new Set();
3010
- const accept = (relative12) => {
3011
- if (seen.has(relative12)) return;
3012
- ensurePathInsideCwd(relative12, cwd);
3013
- seen.add(relative12);
3014
- collected.push(relative12);
3100
+ const accept = (relative13) => {
3101
+ if (seen.has(relative13)) return;
3102
+ ensurePathInsideCwd(relative13, cwd);
3103
+ seen.add(relative13);
3104
+ collected.push(relative13);
3015
3105
  };
3016
3106
  if (paths) {
3017
3107
  for (const p of paths) {
@@ -3054,13 +3144,13 @@ async function resolveValidatorSources(config) {
3054
3144
  pathList.push(match);
3055
3145
  }
3056
3146
  }
3057
- for (const relative12 of pathList) {
3058
- if (seenNames.has(relative12)) continue;
3059
- ensurePathInsideCwd(relative12, cwd);
3060
- const absolute = resolve(cwd, relative12);
3147
+ for (const relative13 of pathList) {
3148
+ if (seenNames.has(relative13)) continue;
3149
+ ensurePathInsideCwd(relative13, cwd);
3150
+ const absolute = resolve(cwd, relative13);
3061
3151
  const text = await readFile(absolute, "utf8");
3062
- seenNames.add(relative12);
3063
- collected.push({ name: relative12, text });
3152
+ seenNames.add(relative13);
3153
+ collected.push({ name: relative13, text });
3064
3154
  }
3065
3155
  return collected;
3066
3156
  }
@@ -3115,12 +3205,12 @@ function formatRemediationBlock(hint) {
3115
3205
  return lines.join("\n");
3116
3206
  }
3117
3207
  function aggregateFolderRules(config) {
3118
- const { inspections, runRules: runRules9 } = config;
3208
+ const { inspections, runRules: runRules10 } = config;
3119
3209
  const violations = [];
3120
3210
  let errorCount = 0;
3121
3211
  let warningCount = 0;
3122
3212
  for (const inspection of inspections) {
3123
- const folderViolations = runRules9(inspection);
3213
+ const folderViolations = runRules10(inspection);
3124
3214
  for (const v of folderViolations) {
3125
3215
  violations.push(v);
3126
3216
  if (v.severity === "error") {
@@ -12739,6 +12829,901 @@ function appendDiagnosticEntry(lines, d) {
12739
12829
  }
12740
12830
  }
12741
12831
 
12832
+ // packages/dbx-cli/validate/src/lib/workspace-validate/index.ts
12833
+ var workspace_validate_exports = {};
12834
+ __export(workspace_validate_exports, {
12835
+ DEPLOY_TARGET_PREFIX: () => DEPLOY_TARGET_PREFIX,
12836
+ WORKSPACE_RULE_GROUPS: () => WORKSPACE_RULE_GROUPS,
12837
+ WorkspaceValidateCode: () => WorkspaceValidateCode,
12838
+ formatResult: () => formatResult11,
12839
+ inspectWorkspace: () => inspectWorkspace,
12840
+ pathEscapesWorkspaceRoot: () => pathEscapesWorkspaceRoot,
12841
+ scanDocumentTargetReferences: () => scanDocumentTargetReferences,
12842
+ scanHostingTargets: () => scanHostingTargets,
12843
+ scanTargetReferences: () => scanTargetReferences,
12844
+ validateWorkspace: () => validateWorkspace
12845
+ });
12846
+
12847
+ // packages/dbx-cli/validate/src/lib/workspace-validate/codes.ts
12848
+ var WorkspaceValidateCode = /* @__PURE__ */ ((WorkspaceValidateCode2) => {
12849
+ WorkspaceValidateCode2["WORKSPACE_TARGET_REF_PROJECT_MISSING"] = "WORKSPACE_TARGET_REF_PROJECT_MISSING";
12850
+ WorkspaceValidateCode2["WORKSPACE_TARGET_REF_TARGET_MISSING"] = "WORKSPACE_TARGET_REF_TARGET_MISSING";
12851
+ WorkspaceValidateCode2["WORKSPACE_TARGET_REF_CONFIGURATION_MISSING"] = "WORKSPACE_TARGET_REF_CONFIGURATION_MISSING";
12852
+ WorkspaceValidateCode2["WORKSPACE_TARGET_OUTPUT_ESCAPES_ROOT"] = "WORKSPACE_TARGET_OUTPUT_ESCAPES_ROOT";
12853
+ WorkspaceValidateCode2["WORKSPACE_DEPLOY_LANE_NO_ENVIRONMENT_SELECTION"] = "WORKSPACE_DEPLOY_LANE_NO_ENVIRONMENT_SELECTION";
12854
+ WorkspaceValidateCode2["WORKSPACE_DEPLOY_ENVIRONMENT_FILE_MISSING"] = "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_MISSING";
12855
+ WorkspaceValidateCode2["WORKSPACE_DEPLOY_ENVIRONMENT_FILE_UNREFERENCED"] = "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_UNREFERENCED";
12856
+ WorkspaceValidateCode2["WORKSPACE_DEPLOY_HOSTING_TARGET_MISSING"] = "WORKSPACE_DEPLOY_HOSTING_TARGET_MISSING";
12857
+ WorkspaceValidateCode2["WORKSPACE_DEPLOY_LANE_ASYMMETRIC"] = "WORKSPACE_DEPLOY_LANE_ASYMMETRIC";
12858
+ return WorkspaceValidateCode2;
12859
+ })(WorkspaceValidateCode || {});
12860
+
12861
+ // packages/dbx-cli/validate/src/lib/workspace-validate/format.ts
12862
+ function formatResult11(result) {
12863
+ return formatFileGroupedResult({
12864
+ title: "Workspace convention validation",
12865
+ summary: `Checked ${result.projectsChecked} project(s) and ${result.referencesChecked} target reference(s) across group(s) [${result.groups.join(", ")}].`,
12866
+ innerKey: (violation) => violation.project,
12867
+ result
12868
+ });
12869
+ }
12870
+
12871
+ // packages/dbx-cli/validate/src/lib/workspace-validate/inspect.ts
12872
+ import { readdir as readdir8, readFile as readFile11, stat as stat10 } from "node:fs/promises";
12873
+ import { basename as basename5, dirname as dirname2, join as join10, relative as relative6, resolve as resolve3 } from "node:path";
12874
+
12875
+ // packages/dbx-cli/validate/src/lib/workspace-validate/references.ts
12876
+ var NX_SUBCOMMANDS = /* @__PURE__ */ new Set(["run", "run-many", "affected", "affected:graph", "affected:test", "affected:build", "graph", "dep-graph", "print-affected", "show", "list", "report", "migrate", "reset", "repair", "generate", "g", "add", "init", "import", "sync", "release", "format", "format:check", "format:write", "daemon", "connect", "login", "logout", "view-logs", "exec", "watch", "documentation", "help", "register", "record", "start-ci-run"]);
12877
+ var INTERPOLATION_PATTERN = /[$`{}()*?]|\\\\/;
12878
+ var TOKEN_LEADING_CHARS = `"'`;
12879
+ var TOKEN_TRAILING_CHARS = `"',;`;
12880
+ var PROJECT_TOKEN_PATTERN = /^[a-zA-Z0-9@][a-zA-Z0-9@/_.-]*$/;
12881
+ var TARGET_TOKEN_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/;
12882
+ function isFlag(token) {
12883
+ return token.startsWith("-");
12884
+ }
12885
+ function isResolvable(token) {
12886
+ return token.length > 0 && !INTERPOLATION_PATTERN.test(token);
12887
+ }
12888
+ function trimTokenPunctuation(raw) {
12889
+ let start = 0;
12890
+ let end = raw.length;
12891
+ while (start < end && TOKEN_LEADING_CHARS.includes(raw[start])) {
12892
+ start += 1;
12893
+ }
12894
+ while (end > start && TOKEN_TRAILING_CHARS.includes(raw[end - 1])) {
12895
+ end -= 1;
12896
+ }
12897
+ return raw.slice(start, end);
12898
+ }
12899
+ function tokenize(text) {
12900
+ const out = [];
12901
+ for (const raw of text.split(/\s+/)) {
12902
+ const token = trimTokenPunctuation(raw);
12903
+ if (token.length > 0) {
12904
+ out.push(token);
12905
+ }
12906
+ }
12907
+ return out;
12908
+ }
12909
+ function isNxBinary(token) {
12910
+ return token === "nx" || token.endsWith("/nx");
12911
+ }
12912
+ function changesToForeignDirectory(tokens) {
12913
+ let result = false;
12914
+ for (let i = 0; i < tokens.length - 1 && !result; i += 1) {
12915
+ if (tokens[i] === "cd") {
12916
+ const destination = tokens[i + 1];
12917
+ result = destination.startsWith("~") || destination.startsWith("/");
12918
+ }
12919
+ }
12920
+ return result;
12921
+ }
12922
+ function parseExplicitForm(tokens, runIndex) {
12923
+ let result;
12924
+ for (let i = runIndex + 1; i < tokens.length; i += 1) {
12925
+ const token = tokens[i];
12926
+ if (isFlag(token)) {
12927
+ continue;
12928
+ }
12929
+ if (isResolvable(token) && token.includes(":")) {
12930
+ const [project, target, configuration] = token.split(":");
12931
+ if (project && target && PROJECT_TOKEN_PATTERN.test(project) && TARGET_TOKEN_PATTERN.test(target)) {
12932
+ result = { project, target, configuration: configuration || void 0, raw: token };
12933
+ }
12934
+ }
12935
+ break;
12936
+ }
12937
+ return result;
12938
+ }
12939
+ function parseShorthandForm(tokens, nxIndex) {
12940
+ const positional = [];
12941
+ for (let i = nxIndex + 1; i < tokens.length && positional.length < 2; i += 1) {
12942
+ const token = tokens[i];
12943
+ if (!isFlag(token)) {
12944
+ positional.push(token);
12945
+ }
12946
+ }
12947
+ let result;
12948
+ const [target, project] = positional;
12949
+ if (target !== void 0 && project !== void 0 && !NX_SUBCOMMANDS.has(target) && !target.includes(":") && isResolvable(target) && isResolvable(project) && TARGET_TOKEN_PATTERN.test(target) && PROJECT_TOKEN_PATTERN.test(project)) {
12950
+ result = { project, target, raw: `${target} ${project}` };
12951
+ }
12952
+ return result;
12953
+ }
12954
+ function scanTargetReferences(input) {
12955
+ const { text, sourceFile, sourceTarget, line } = input;
12956
+ const references = [];
12957
+ const tokens = tokenize(text);
12958
+ if (changesToForeignDirectory(tokens)) {
12959
+ return references;
12960
+ }
12961
+ for (let i = 0; i < tokens.length; i += 1) {
12962
+ if (!isNxBinary(tokens[i])) {
12963
+ continue;
12964
+ }
12965
+ let firstPositional;
12966
+ let firstPositionalIndex = -1;
12967
+ for (let j = i + 1; j < tokens.length; j += 1) {
12968
+ if (!isFlag(tokens[j])) {
12969
+ firstPositional = tokens[j];
12970
+ firstPositionalIndex = j;
12971
+ break;
12972
+ }
12973
+ }
12974
+ if (firstPositional === void 0) {
12975
+ continue;
12976
+ }
12977
+ if (firstPositional === "run") {
12978
+ const parsed = parseExplicitForm(tokens, firstPositionalIndex);
12979
+ if (parsed) {
12980
+ references.push({ project: parsed.project, target: parsed.target, configuration: parsed.configuration, sourceFile, line, sourceTarget, raw: parsed.raw });
12981
+ }
12982
+ } else if (!NX_SUBCOMMANDS.has(firstPositional)) {
12983
+ const parsed = parseShorthandForm(tokens, i);
12984
+ if (parsed) {
12985
+ references.push({ project: parsed.project, target: parsed.target, configuration: void 0, sourceFile, line, sourceTarget, raw: parsed.raw });
12986
+ }
12987
+ }
12988
+ }
12989
+ return references;
12990
+ }
12991
+ function stripComment(line) {
12992
+ const trimmed = line.trimStart();
12993
+ let result;
12994
+ if (trimmed.startsWith("#")) {
12995
+ result = "";
12996
+ } else {
12997
+ const index = line.indexOf(" #");
12998
+ result = index === -1 ? line : line.slice(0, index);
12999
+ }
13000
+ return result;
13001
+ }
13002
+ function scanDocumentTargetReferences(input) {
13003
+ const { text, sourceFile } = input;
13004
+ const references = [];
13005
+ const lines = text.split("\n");
13006
+ for (const [index, line] of lines.entries()) {
13007
+ for (const reference of scanTargetReferences({ text: stripComment(line), sourceFile, line: index + 1 })) {
13008
+ references.push(reference);
13009
+ }
13010
+ }
13011
+ return references;
13012
+ }
13013
+ function scanHostingTargets(command) {
13014
+ const out = [];
13015
+ const pattern = /hosting:([a-zA-Z0-9_-]+)/g;
13016
+ let match = pattern.exec(command);
13017
+ while (match !== null) {
13018
+ out.push(match[1]);
13019
+ match = pattern.exec(command);
13020
+ }
13021
+ return out;
13022
+ }
13023
+
13024
+ // packages/dbx-cli/validate/src/lib/workspace-validate/inspect.ts
13025
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "tmp", "coverage", ".nx", ".git", ".firebase", ".angular", ".cache", "templates"]);
13026
+ var MAX_WALK_DEPTH = 4;
13027
+ var PROJECT_CONFIG_FILENAME = "project.json";
13028
+ var ENVIRONMENTS_SUBPATH = join10("src", "environments");
13029
+ var ENVIRONMENT_FILE_PATTERN = /^environment\..+\.ts$/;
13030
+ var EXTRA_REFERENCE_SOURCES = [".circleci/config.yml", ".circleci/config.yaml", ".github/workflows"];
13031
+ async function pathExists(path) {
13032
+ let result;
13033
+ try {
13034
+ await stat10(path);
13035
+ result = true;
13036
+ } catch {
13037
+ result = false;
13038
+ }
13039
+ return result;
13040
+ }
13041
+ async function readJsonFile(path) {
13042
+ let result;
13043
+ try {
13044
+ const text = await readFile11(path, "utf8");
13045
+ const parsed = JSON.parse(text);
13046
+ result = parsed !== null && typeof parsed === "object" ? parsed : void 0;
13047
+ } catch {
13048
+ result = void 0;
13049
+ }
13050
+ return result;
13051
+ }
13052
+ async function collectProjectConfigPaths(config) {
13053
+ const { workspaceRoot, relativeDir, depth } = config;
13054
+ const collected = [];
13055
+ const absoluteDir = join10(workspaceRoot, relativeDir);
13056
+ const entries = await readdir8(absoluteDir, { withFileTypes: true }).catch(() => []);
13057
+ for (const entry of entries) {
13058
+ if (entry.isFile() && entry.name === PROJECT_CONFIG_FILENAME) {
13059
+ collected.push(relativeDir === "" ? PROJECT_CONFIG_FILENAME : join10(relativeDir, PROJECT_CONFIG_FILENAME));
13060
+ } else if (entry.isDirectory() && depth < MAX_WALK_DEPTH && !SKIPPED_DIRECTORIES.has(entry.name) && !entry.name.startsWith("firebase-export-")) {
13061
+ const nested = await collectProjectConfigPaths({ workspaceRoot, relativeDir: relativeDir === "" ? entry.name : join10(relativeDir, entry.name), depth: depth + 1 });
13062
+ for (const path of nested) {
13063
+ collected.push(path);
13064
+ }
13065
+ }
13066
+ }
13067
+ return collected;
13068
+ }
13069
+ function extractCommands(options) {
13070
+ const collected = [];
13071
+ const single = options["command"];
13072
+ if (typeof single === "string") {
13073
+ collected.push(single);
13074
+ }
13075
+ const many = options["commands"];
13076
+ if (Array.isArray(many)) {
13077
+ for (const entry of many) {
13078
+ if (typeof entry === "string") {
13079
+ collected.push(entry);
13080
+ } else if (entry !== null && typeof entry === "object") {
13081
+ const command = entry["command"];
13082
+ if (typeof command === "string") {
13083
+ collected.push(command);
13084
+ }
13085
+ }
13086
+ }
13087
+ }
13088
+ return collected;
13089
+ }
13090
+ async function resolveFileReplacements(config) {
13091
+ const { workspaceRoot, raw } = config;
13092
+ const collected = [];
13093
+ if (Array.isArray(raw)) {
13094
+ for (const entry of raw) {
13095
+ if (entry === null || typeof entry !== "object") {
13096
+ continue;
13097
+ }
13098
+ const record = entry;
13099
+ const replace = record["replace"];
13100
+ const withPath = record["with"];
13101
+ if (typeof replace === "string" && typeof withPath === "string") {
13102
+ collected.push({
13103
+ replace,
13104
+ with: withPath,
13105
+ replaceExists: await pathExists(join10(workspaceRoot, replace)),
13106
+ withExists: await pathExists(join10(workspaceRoot, withPath))
13107
+ });
13108
+ }
13109
+ }
13110
+ }
13111
+ return collected;
13112
+ }
13113
+ async function resolveConfigurations(config) {
13114
+ const { workspaceRoot, raw } = config;
13115
+ const collected = [];
13116
+ if (raw !== null && typeof raw === "object") {
13117
+ for (const [name, value] of Object.entries(raw)) {
13118
+ const options = value !== null && typeof value === "object" ? value : {};
13119
+ const esbuildConfigValue = options["esbuildConfig"];
13120
+ const esbuildConfig = typeof esbuildConfigValue === "string" ? esbuildConfigValue : void 0;
13121
+ collected.push({
13122
+ name,
13123
+ fileReplacements: await resolveFileReplacements({ workspaceRoot, raw: options["fileReplacements"] }),
13124
+ esbuildConfig,
13125
+ esbuildConfigExists: esbuildConfig === void 0 ? false : await pathExists(join10(workspaceRoot, esbuildConfig))
13126
+ });
13127
+ }
13128
+ }
13129
+ return collected;
13130
+ }
13131
+ async function resolveTargets(config) {
13132
+ const { workspaceRoot, raw } = config;
13133
+ const collected = [];
13134
+ if (raw !== null && typeof raw === "object") {
13135
+ for (const [name, value] of Object.entries(raw)) {
13136
+ const target = value !== null && typeof value === "object" ? value : {};
13137
+ const executorValue = target["executor"];
13138
+ const defaultConfigurationValue = target["defaultConfiguration"];
13139
+ const optionsValue = target["options"];
13140
+ const options = optionsValue !== null && typeof optionsValue === "object" ? optionsValue : {};
13141
+ const outputsValue = target["outputs"];
13142
+ collected.push({
13143
+ name,
13144
+ executor: typeof executorValue === "string" ? executorValue : void 0,
13145
+ configurations: await resolveConfigurations({ workspaceRoot, raw: target["configurations"] }),
13146
+ defaultConfiguration: typeof defaultConfigurationValue === "string" ? defaultConfigurationValue : void 0,
13147
+ outputs: Array.isArray(outputsValue) ? outputsValue.filter((o) => typeof o === "string") : [],
13148
+ options,
13149
+ commands: extractCommands(options)
13150
+ });
13151
+ }
13152
+ }
13153
+ return collected;
13154
+ }
13155
+ async function collectEnvironmentFiles(config) {
13156
+ const { workspaceRoot, projectRoot } = config;
13157
+ const relativeDir = projectRoot === "" ? ENVIRONMENTS_SUBPATH : join10(projectRoot, ENVIRONMENTS_SUBPATH);
13158
+ const collected = [];
13159
+ const entries = await readdir8(join10(workspaceRoot, relativeDir), { withFileTypes: true }).catch(() => []);
13160
+ for (const entry of entries) {
13161
+ if (entry.isFile() && ENVIRONMENT_FILE_PATTERN.test(entry.name)) {
13162
+ collected.push(join10(relativeDir, entry.name));
13163
+ }
13164
+ }
13165
+ return collected.sort((a, b) => a.localeCompare(b));
13166
+ }
13167
+ var ENVIRONMENT_MENTION_PATTERN = /environment(?:\.[A-Za-z0-9_-]+)*\.ts/g;
13168
+ async function collectReferencedEnvironmentFiles(config) {
13169
+ const { workspaceRoot, targets, environmentFiles } = config;
13170
+ const referenced = /* @__PURE__ */ new Set();
13171
+ const esbuildConfigPaths = /* @__PURE__ */ new Set();
13172
+ for (const target of targets) {
13173
+ const baseEsbuildConfig = target.options["esbuildConfig"];
13174
+ if (typeof baseEsbuildConfig === "string") {
13175
+ esbuildConfigPaths.add(baseEsbuildConfig);
13176
+ }
13177
+ for (const configuration of target.configurations) {
13178
+ for (const replacement of configuration.fileReplacements) {
13179
+ referenced.add(replacement.with);
13180
+ }
13181
+ if (configuration.esbuildConfig !== void 0) {
13182
+ esbuildConfigPaths.add(configuration.esbuildConfig);
13183
+ }
13184
+ }
13185
+ }
13186
+ const mentionedBasenames = /* @__PURE__ */ new Set();
13187
+ for (const configPath of esbuildConfigPaths) {
13188
+ const text = await readFile11(join10(workspaceRoot, configPath), "utf8").catch(() => void 0);
13189
+ if (text === void 0) {
13190
+ continue;
13191
+ }
13192
+ for (const match of text.matchAll(ENVIRONMENT_MENTION_PATTERN)) {
13193
+ mentionedBasenames.add(match[0]);
13194
+ }
13195
+ }
13196
+ for (const environmentFile of environmentFiles) {
13197
+ if (mentionedBasenames.has(basename5(environmentFile))) {
13198
+ referenced.add(environmentFile);
13199
+ }
13200
+ }
13201
+ return Array.from(referenced).sort((a, b) => a.localeCompare(b));
13202
+ }
13203
+ async function inspectProject(config) {
13204
+ const { workspaceRoot, configFile } = config;
13205
+ const parsed = await readJsonFile(join10(workspaceRoot, configFile));
13206
+ if (parsed === void 0) {
13207
+ return void 0;
13208
+ }
13209
+ const projectRoot = dirname2(configFile) === "." ? "" : dirname2(configFile);
13210
+ const nameValue = parsed["name"];
13211
+ const fallbackNameDir = projectRoot === "" ? workspaceRoot : projectRoot;
13212
+ const name = typeof nameValue === "string" && nameValue.length > 0 ? nameValue : basename5(fallbackNameDir);
13213
+ const targets = await resolveTargets({ workspaceRoot, raw: parsed["targets"] });
13214
+ const environmentFiles = await collectEnvironmentFiles({ workspaceRoot, projectRoot });
13215
+ return {
13216
+ name,
13217
+ root: projectRoot,
13218
+ configFile,
13219
+ targets,
13220
+ environmentFiles,
13221
+ referencedEnvironmentFiles: await collectReferencedEnvironmentFiles({ workspaceRoot, targets, environmentFiles })
13222
+ };
13223
+ }
13224
+ async function inspectNxConfig(workspaceRoot) {
13225
+ const parsed = await readJsonFile(join10(workspaceRoot, "nx.json"));
13226
+ const targetDefaultNames = [];
13227
+ const plugins = [];
13228
+ const targetDefaults = parsed?.["targetDefaults"];
13229
+ if (targetDefaults !== null && typeof targetDefaults === "object") {
13230
+ for (const key of Object.keys(targetDefaults)) {
13231
+ if (!key.includes(":")) {
13232
+ targetDefaultNames.push(key);
13233
+ }
13234
+ }
13235
+ }
13236
+ const pluginsValue = parsed?.["plugins"];
13237
+ if (Array.isArray(pluginsValue)) {
13238
+ for (const entry of pluginsValue) {
13239
+ if (typeof entry === "string") {
13240
+ plugins.push(entry);
13241
+ } else if (entry !== null && typeof entry === "object") {
13242
+ const plugin = entry["plugin"];
13243
+ if (typeof plugin === "string") {
13244
+ plugins.push(plugin);
13245
+ }
13246
+ }
13247
+ }
13248
+ }
13249
+ return { targetDefaultNames, plugins };
13250
+ }
13251
+ async function inspectFirebaseConfig(workspaceRoot) {
13252
+ const parsed = await readJsonFile(join10(workspaceRoot, "firebase.json"));
13253
+ if (parsed === void 0) {
13254
+ return { present: false, hostingTargets: [] };
13255
+ }
13256
+ const hostingValue = parsed["hosting"];
13257
+ const singleHostingEntry = hostingValue !== null && typeof hostingValue === "object" ? [hostingValue] : [];
13258
+ const entries = Array.isArray(hostingValue) ? hostingValue : singleHostingEntry;
13259
+ const hostingTargets = [];
13260
+ for (const entry of entries) {
13261
+ if (entry === null || typeof entry !== "object") {
13262
+ continue;
13263
+ }
13264
+ const record = entry;
13265
+ const target = record["target"];
13266
+ const site = record["site"];
13267
+ if (typeof target === "string") {
13268
+ hostingTargets.push(target);
13269
+ } else if (typeof site === "string") {
13270
+ hostingTargets.push(site);
13271
+ }
13272
+ }
13273
+ return { present: true, hostingTargets };
13274
+ }
13275
+ async function collectExtraReferenceSources(workspaceRoot) {
13276
+ const collected = [];
13277
+ for (const candidate of EXTRA_REFERENCE_SOURCES) {
13278
+ const absolute = join10(workspaceRoot, candidate);
13279
+ let stats;
13280
+ try {
13281
+ stats = await stat10(absolute);
13282
+ } catch {
13283
+ continue;
13284
+ }
13285
+ if (stats.isFile()) {
13286
+ collected.push(candidate);
13287
+ } else if (stats.isDirectory()) {
13288
+ const entries = await readdir8(absolute, { withFileTypes: true });
13289
+ for (const entry of entries) {
13290
+ if (entry.isFile() && /\.ya?ml$/.test(entry.name)) {
13291
+ collected.push(join10(candidate, entry.name));
13292
+ }
13293
+ }
13294
+ }
13295
+ }
13296
+ const rootEntries = await readdir8(workspaceRoot, { withFileTypes: true }).catch(() => []);
13297
+ for (const entry of rootEntries) {
13298
+ if (entry.isFile() && entry.name.endsWith(".sh")) {
13299
+ collected.push(entry.name);
13300
+ }
13301
+ }
13302
+ return collected.sort((a, b) => a.localeCompare(b));
13303
+ }
13304
+ async function collectPackageScriptReferences(workspaceRoot) {
13305
+ const parsed = await readJsonFile(join10(workspaceRoot, "package.json"));
13306
+ const scripts = parsed?.["scripts"];
13307
+ const collected = [];
13308
+ if (scripts !== null && typeof scripts === "object") {
13309
+ for (const [scriptName, value] of Object.entries(scripts)) {
13310
+ if (typeof value === "string") {
13311
+ for (const reference of scanTargetReferences({ text: value, sourceFile: "package.json", sourceTarget: scriptName })) {
13312
+ collected.push(reference);
13313
+ }
13314
+ }
13315
+ }
13316
+ }
13317
+ return collected;
13318
+ }
13319
+ async function inspectWorkspace(input) {
13320
+ const workspaceRoot = resolve3(input.workspaceRoot);
13321
+ const configPaths = await collectProjectConfigPaths({ workspaceRoot, relativeDir: "", depth: 0 });
13322
+ const projects = [];
13323
+ const references = [];
13324
+ for (const configFile of [...configPaths].sort((a, b) => a.localeCompare(b))) {
13325
+ const project = await inspectProject({ workspaceRoot, configFile });
13326
+ if (project === void 0) {
13327
+ continue;
13328
+ }
13329
+ projects.push(project);
13330
+ for (const target of project.targets) {
13331
+ for (const command of target.commands) {
13332
+ for (const reference of scanTargetReferences({ text: command, sourceFile: configFile, sourceTarget: target.name })) {
13333
+ references.push(reference);
13334
+ }
13335
+ }
13336
+ }
13337
+ }
13338
+ for (const sourceFile of await collectExtraReferenceSources(workspaceRoot)) {
13339
+ const text = await readFile11(join10(workspaceRoot, sourceFile), "utf8").catch(() => void 0);
13340
+ if (text !== void 0) {
13341
+ for (const reference of scanDocumentTargetReferences({ text, sourceFile })) {
13342
+ references.push(reference);
13343
+ }
13344
+ }
13345
+ }
13346
+ for (const reference of await collectPackageScriptReferences(workspaceRoot)) {
13347
+ references.push(reference);
13348
+ }
13349
+ return {
13350
+ workspaceRoot,
13351
+ projects,
13352
+ references,
13353
+ firebase: await inspectFirebaseConfig(workspaceRoot),
13354
+ nx: await inspectNxConfig(workspaceRoot)
13355
+ };
13356
+ }
13357
+ function pathEscapesWorkspaceRoot(config) {
13358
+ const { workspaceRoot, candidate } = config;
13359
+ const resolved = resolve3(workspaceRoot, candidate);
13360
+ const rel = relative6(workspaceRoot, resolved);
13361
+ return rel.startsWith("..");
13362
+ }
13363
+
13364
+ // packages/dbx-cli/validate/src/lib/workspace-validate/types.ts
13365
+ var WORKSPACE_RULE_GROUPS = ["targets", "deploy"];
13366
+ var DEPLOY_TARGET_PREFIX = "ci-deploy-";
13367
+
13368
+ // packages/dbx-cli/validate/src/lib/workspace-validate/rules.ts
13369
+ var FILE_REPLACEMENT_EXECUTORS = /* @__PURE__ */ new Set(["@nx/angular:application", "@nx/angular:browser-esbuild", "@angular/build:application", "@angular-devkit/build-angular:application", "@angular-devkit/build-angular:browser"]);
13370
+ var ESBUILD_CONFIG_EXECUTORS = /* @__PURE__ */ new Set(["@nx/esbuild:esbuild"]);
13371
+ var PLUGIN_INFERRED_TARGETS = [
13372
+ { plugin: "@nx/eslint/plugin", targets: ["lint"] },
13373
+ { plugin: "@nx/vitest", targets: ["test", "test-ci"] },
13374
+ { plugin: "@nx/vite", targets: ["build", "serve", "preview", "test"] },
13375
+ { plugin: "@nx/jest", targets: ["test"] },
13376
+ { plugin: "@nx/webpack/plugin", targets: ["build", "serve", "preview"] },
13377
+ { plugin: "@nx/rollup/plugin", targets: ["build"] },
13378
+ { plugin: "@nx/js/typescript", targets: ["typecheck", "build"] },
13379
+ { plugin: "@nx/playwright/plugin", targets: ["e2e", "e2e-ci"] },
13380
+ { plugin: "@nx/cypress/plugin", targets: ["e2e", "component-test"] },
13381
+ { plugin: "@nx/next/plugin", targets: ["build", "dev", "start", "serve-static"] }
13382
+ ];
13383
+ var FILE_EXTENSION_PATTERN = /\.(?:ts|tsx|js|mjs|cjs|json|sh|ya?ml|md|html|scss|css)$/;
13384
+ var DEPLOY_LANE_PATTERN = new RegExp(`^${DEPLOY_TARGET_PREFIX}([a-z0-9]+)$`);
13385
+ function pushViolation11(buffer, violation) {
13386
+ const severity = violation.severity ?? "error";
13387
+ const filled = {
13388
+ code: violation.code,
13389
+ severity,
13390
+ message: violation.message,
13391
+ group: violation.group,
13392
+ project: violation.project,
13393
+ file: violation.file,
13394
+ line: violation.line,
13395
+ remediation: attachRemediation(violation.code)
13396
+ };
13397
+ buffer.push(filled);
13398
+ }
13399
+ function looksLikeProjectName(token) {
13400
+ return !token.includes("/") && !token.includes("\\") && !FILE_EXTENSION_PATTERN.test(token);
13401
+ }
13402
+ function isExemptTargetName(config) {
13403
+ const { targetName, nx } = config;
13404
+ let result = nx.targetDefaultNames.includes(targetName);
13405
+ if (!result) {
13406
+ const plugins = new Set(nx.plugins);
13407
+ result = PLUGIN_INFERRED_TARGETS.some((entry) => plugins.has(entry.plugin) && entry.targets.includes(targetName));
13408
+ }
13409
+ return result;
13410
+ }
13411
+ function substituteOutputTokens(config) {
13412
+ const { entry, options, projectRoot } = config;
13413
+ let unresolved = false;
13414
+ const withOptions = entry.replaceAll(/\{options\.(\w+)\}/g, (_match, key) => {
13415
+ const value = options[key];
13416
+ let replacement;
13417
+ if (typeof value === "string") {
13418
+ replacement = value;
13419
+ } else if (typeof value === "number" || typeof value === "boolean") {
13420
+ replacement = String(value);
13421
+ } else {
13422
+ unresolved = true;
13423
+ replacement = "";
13424
+ }
13425
+ return replacement;
13426
+ });
13427
+ const projectRootValue = projectRoot === "" ? "." : projectRoot;
13428
+ return unresolved ? void 0 : withOptions.replaceAll("{workspaceRoot}", ".").replaceAll("{projectRoot}", projectRootValue);
13429
+ }
13430
+ function environmentSelectionState(config) {
13431
+ const { executor, configuration } = config;
13432
+ let result;
13433
+ if (executor !== void 0 && FILE_REPLACEMENT_EXECUTORS.has(executor)) {
13434
+ result = configuration.fileReplacements.length > 0 ? "ok" : "missing";
13435
+ } else if (executor !== void 0 && ESBUILD_CONFIG_EXECUTORS.has(executor)) {
13436
+ result = configuration.esbuildConfig === void 0 ? "missing" : "ok";
13437
+ } else {
13438
+ result = "unknown";
13439
+ }
13440
+ return result;
13441
+ }
13442
+ function environmentSelectionOptionName(executor) {
13443
+ return executor !== void 0 && ESBUILD_CONFIG_EXECUTORS.has(executor) ? "esbuildConfig" : "fileReplacements";
13444
+ }
13445
+ function indexTargets(project) {
13446
+ const map = /* @__PURE__ */ new Map();
13447
+ for (const target of project.targets) {
13448
+ map.set(target.name, target);
13449
+ }
13450
+ return map;
13451
+ }
13452
+ function checkTargetReferences(config) {
13453
+ const { inspection, targetsByProject, violations } = config;
13454
+ const group = "targets";
13455
+ for (const reference of inspection.references) {
13456
+ const targets = targetsByProject.get(reference.project);
13457
+ if (targets === void 0) {
13458
+ if (looksLikeProjectName(reference.project)) {
13459
+ pushViolation11(violations, {
13460
+ code: "WORKSPACE_TARGET_REF_PROJECT_MISSING",
13461
+ message: `\`${reference.raw}\` references project \`${reference.project}\`, which the workspace does not declare.`,
13462
+ group,
13463
+ project: reference.project,
13464
+ file: reference.sourceFile,
13465
+ line: reference.line
13466
+ });
13467
+ }
13468
+ continue;
13469
+ }
13470
+ const target = targets.get(reference.target);
13471
+ if (target === void 0) {
13472
+ if (!isExemptTargetName({ targetName: reference.target, nx: inspection.nx })) {
13473
+ pushViolation11(violations, {
13474
+ code: "WORKSPACE_TARGET_REF_TARGET_MISSING",
13475
+ message: `\`${reference.raw}\` references target \`${reference.target}\` on project \`${reference.project}\`, which declares no such target.`,
13476
+ group,
13477
+ project: reference.project,
13478
+ file: reference.sourceFile,
13479
+ line: reference.line
13480
+ });
13481
+ }
13482
+ continue;
13483
+ }
13484
+ if (reference.configuration !== void 0 && target.configurations.length > 0 && !target.configurations.some((configuration) => configuration.name === reference.configuration)) {
13485
+ const declared = target.configurations.map((configuration) => configuration.name).join(", ");
13486
+ const fallback = target.defaultConfiguration === void 0 ? "no configuration at all \u2014 the target runs with its base options, so every configuration-only option is silently dropped" : `\`${target.defaultConfiguration}\` (this target's \`defaultConfiguration\`)`;
13487
+ pushViolation11(violations, {
13488
+ code: "WORKSPACE_TARGET_REF_CONFIGURATION_MISSING",
13489
+ message: `\`${reference.raw}\` requests configuration \`${reference.configuration}\` of \`${reference.project}:${reference.target}\`, which declares only [${declared}]. Nx does not error here \u2014 it silently builds ${fallback}.`,
13490
+ group,
13491
+ project: reference.project,
13492
+ file: reference.sourceFile,
13493
+ line: reference.line
13494
+ });
13495
+ }
13496
+ }
13497
+ }
13498
+ function checkTargetOutputs(config) {
13499
+ const { inspection, violations } = config;
13500
+ const group = "targets";
13501
+ for (const project of inspection.projects) {
13502
+ for (const target of project.targets) {
13503
+ for (const entry of target.outputs) {
13504
+ const substituted = substituteOutputTokens({ entry, options: target.options, projectRoot: project.root });
13505
+ if (substituted === void 0) {
13506
+ continue;
13507
+ }
13508
+ if (pathEscapesWorkspaceRoot({ workspaceRoot: inspection.workspaceRoot, candidate: substituted })) {
13509
+ pushViolation11(violations, {
13510
+ code: "WORKSPACE_TARGET_OUTPUT_ESCAPES_ROOT",
13511
+ message: `Target \`${project.name}:${target.name}\` declares output \`${entry}\`, which resolves to \`${substituted}\` \u2014 outside the workspace root. Nx cannot cache it and reports the task as flaky instead of failing.`,
13512
+ group,
13513
+ project: project.name,
13514
+ file: project.configFile,
13515
+ line: void 0
13516
+ });
13517
+ }
13518
+ }
13519
+ }
13520
+ }
13521
+ }
13522
+ function checkDeployLaneEnvironmentSelection(config) {
13523
+ const { inspection, targetsByProject, violations } = config;
13524
+ const group = "deploy";
13525
+ for (const project of inspection.projects) {
13526
+ const targets = targetsByProject.get(project.name);
13527
+ if (targets === void 0) {
13528
+ continue;
13529
+ }
13530
+ const deployTargetNames = new Set(project.targets.filter((target) => target.name.startsWith(DEPLOY_TARGET_PREFIX)).map((target) => target.name));
13531
+ for (const reference of inspection.references) {
13532
+ if (reference.sourceFile !== project.configFile || reference.sourceTarget === void 0 || !deployTargetNames.has(reference.sourceTarget) || reference.project !== project.name || reference.configuration === void 0) {
13533
+ continue;
13534
+ }
13535
+ const buildTarget = targets.get(reference.target);
13536
+ const configuration = buildTarget?.configurations.find((entry) => entry.name === reference.configuration);
13537
+ if (buildTarget === void 0 || configuration === void 0) {
13538
+ continue;
13539
+ }
13540
+ if (environmentSelectionState({ executor: buildTarget.executor, configuration }) === "missing") {
13541
+ pushViolation11(violations, {
13542
+ code: "WORKSPACE_DEPLOY_LANE_NO_ENVIRONMENT_SELECTION",
13543
+ message: `\`${project.name}:${reference.sourceTarget}\` builds \`${reference.target}:${configuration.name}\`, but that configuration declares no \`${environmentSelectionOptionName(buildTarget.executor)}\` \u2014 the lane deploys the unreplaced \`environment.ts\`.`,
13544
+ group,
13545
+ project: project.name,
13546
+ file: project.configFile,
13547
+ line: void 0
13548
+ });
13549
+ }
13550
+ }
13551
+ }
13552
+ }
13553
+ function checkConfigurationPaths(config) {
13554
+ const { inspection, violations } = config;
13555
+ const group = "deploy";
13556
+ for (const project of inspection.projects) {
13557
+ for (const target of project.targets) {
13558
+ for (const configuration of target.configurations) {
13559
+ for (const replacement of configuration.fileReplacements) {
13560
+ if (!replacement.replaceExists) {
13561
+ pushViolation11(violations, {
13562
+ code: "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_MISSING",
13563
+ message: `\`${project.name}:${target.name}:${configuration.name}\` replaces \`${replacement.replace}\`, which does not exist.`,
13564
+ group,
13565
+ project: project.name,
13566
+ file: project.configFile,
13567
+ line: void 0
13568
+ });
13569
+ }
13570
+ if (!replacement.withExists) {
13571
+ pushViolation11(violations, {
13572
+ code: "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_MISSING",
13573
+ message: `\`${project.name}:${target.name}:${configuration.name}\` replaces with \`${replacement.with}\`, which does not exist.`,
13574
+ group,
13575
+ project: project.name,
13576
+ file: project.configFile,
13577
+ line: void 0
13578
+ });
13579
+ }
13580
+ }
13581
+ if (configuration.esbuildConfig !== void 0 && !configuration.esbuildConfigExists) {
13582
+ pushViolation11(violations, {
13583
+ code: "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_MISSING",
13584
+ message: `\`${project.name}:${target.name}:${configuration.name}\` sets \`esbuildConfig\` to \`${configuration.esbuildConfig}\`, which does not exist.`,
13585
+ group,
13586
+ project: project.name,
13587
+ file: project.configFile,
13588
+ line: void 0
13589
+ });
13590
+ }
13591
+ }
13592
+ }
13593
+ }
13594
+ }
13595
+ function checkUnreferencedEnvironmentFiles(config) {
13596
+ const { inspection, violations } = config;
13597
+ const group = "deploy";
13598
+ for (const project of inspection.projects) {
13599
+ const referenced = new Set(project.referencedEnvironmentFiles);
13600
+ for (const environmentFile of project.environmentFiles) {
13601
+ if (!referenced.has(environmentFile)) {
13602
+ pushViolation11(violations, {
13603
+ code: "WORKSPACE_DEPLOY_ENVIRONMENT_FILE_UNREFERENCED",
13604
+ message: `\`${environmentFile}\` is not selected by any build configuration of \`${project.name}\` \u2014 it is edited and reviewed but never compiled.`,
13605
+ severity: "warning",
13606
+ group,
13607
+ project: project.name,
13608
+ file: project.configFile,
13609
+ line: void 0
13610
+ });
13611
+ }
13612
+ }
13613
+ }
13614
+ }
13615
+ function checkHostingTargets(config) {
13616
+ const { inspection, violations } = config;
13617
+ const group = "deploy";
13618
+ if (!inspection.firebase.present) {
13619
+ return;
13620
+ }
13621
+ const declared = new Set(inspection.firebase.hostingTargets);
13622
+ for (const project of inspection.projects) {
13623
+ for (const target of project.targets) {
13624
+ for (const command of target.commands) {
13625
+ for (const hostingTarget of scanHostingTargets(command)) {
13626
+ if (!declared.has(hostingTarget)) {
13627
+ pushViolation11(violations, {
13628
+ code: "WORKSPACE_DEPLOY_HOSTING_TARGET_MISSING",
13629
+ message: `\`${project.name}:${target.name}\` deploys \`hosting:${hostingTarget}\`, which \`firebase.json\` does not declare (declared: [${Array.from(declared).join(", ") || "none"}]).`,
13630
+ group,
13631
+ project: project.name,
13632
+ file: project.configFile,
13633
+ line: void 0
13634
+ });
13635
+ }
13636
+ }
13637
+ }
13638
+ }
13639
+ }
13640
+ }
13641
+ function checkDeployLaneSymmetry(config) {
13642
+ const { inspection, violations } = config;
13643
+ const group = "deploy";
13644
+ const lanesByProject = /* @__PURE__ */ new Map();
13645
+ const allLanes = /* @__PURE__ */ new Set();
13646
+ for (const project of inspection.projects) {
13647
+ const lanes = /* @__PURE__ */ new Set();
13648
+ for (const target of project.targets) {
13649
+ const match = DEPLOY_LANE_PATTERN.exec(target.name);
13650
+ if (match !== null) {
13651
+ lanes.add(match[1]);
13652
+ allLanes.add(match[1]);
13653
+ }
13654
+ }
13655
+ if (lanes.size > 0) {
13656
+ lanesByProject.set(project.name, lanes);
13657
+ }
13658
+ }
13659
+ if (lanesByProject.size < 2) {
13660
+ return;
13661
+ }
13662
+ for (const project of inspection.projects) {
13663
+ const lanes = lanesByProject.get(project.name);
13664
+ if (lanes === void 0) {
13665
+ continue;
13666
+ }
13667
+ for (const lane of Array.from(allLanes).sort((a, b) => a.localeCompare(b))) {
13668
+ if (!lanes.has(lane)) {
13669
+ pushViolation11(violations, {
13670
+ code: "WORKSPACE_DEPLOY_LANE_ASYMMETRIC",
13671
+ message: `\`${project.name}\` deploys [${Array.from(lanes).sort((a, b) => a.localeCompare(b)).join(", ")}] but has no \`${DEPLOY_TARGET_PREFIX}${lane}\`, while a sibling project deploys \`${lane}\`.`,
13672
+ severity: "warning",
13673
+ group,
13674
+ project: project.name,
13675
+ file: project.configFile,
13676
+ line: void 0
13677
+ });
13678
+ }
13679
+ }
13680
+ }
13681
+ }
13682
+ function runRules9(input) {
13683
+ const { inspection, groups } = input;
13684
+ const violations = [];
13685
+ const targetsByProject = /* @__PURE__ */ new Map();
13686
+ for (const project of inspection.projects) {
13687
+ targetsByProject.set(project.name, indexTargets(project));
13688
+ }
13689
+ if (groups.includes("targets")) {
13690
+ checkTargetReferences({ inspection, targetsByProject, violations });
13691
+ checkTargetOutputs({ inspection, violations });
13692
+ }
13693
+ if (groups.includes("deploy")) {
13694
+ checkDeployLaneEnvironmentSelection({ inspection, targetsByProject, violations });
13695
+ checkConfigurationPaths({ inspection, violations });
13696
+ checkUnreferencedEnvironmentFiles({ inspection, violations });
13697
+ checkHostingTargets({ inspection, violations });
13698
+ checkDeployLaneSymmetry({ inspection, violations });
13699
+ }
13700
+ return violations;
13701
+ }
13702
+
13703
+ // packages/dbx-cli/validate/src/lib/workspace-validate/validate.ts
13704
+ function validateWorkspace(input) {
13705
+ const { inspection } = input;
13706
+ const groups = input.groups === void 0 || input.groups.length === 0 ? WORKSPACE_RULE_GROUPS : input.groups;
13707
+ const violations = runRules9({ inspection, groups });
13708
+ let errorCount = 0;
13709
+ let warningCount = 0;
13710
+ for (const violation of violations) {
13711
+ if (violation.severity === "error") {
13712
+ errorCount += 1;
13713
+ } else {
13714
+ warningCount += 1;
13715
+ }
13716
+ }
13717
+ return {
13718
+ violations,
13719
+ errorCount,
13720
+ warningCount,
13721
+ projectsChecked: inspection.projects.length,
13722
+ referencesChecked: inspection.references.length,
13723
+ groups
13724
+ };
13725
+ }
13726
+
12742
13727
  // packages/dbx-cli/validate/src/lib/model-list-component/index.ts
12743
13728
  var model_list_component_exports = {};
12744
13729
  __export(model_list_component_exports, {
@@ -12749,8 +13734,8 @@ __export(model_list_component_exports, {
12749
13734
  });
12750
13735
 
12751
13736
  // packages/dbx-cli/model-test/src/lib/fixture/inspect.ts
12752
- import { readFile as readFile11 } from "node:fs/promises";
12753
- import { join as join10 } from "node:path";
13737
+ import { readFile as readFile12 } from "node:fs/promises";
13738
+ import { join as join11 } from "node:path";
12754
13739
 
12755
13740
  // packages/dbx-cli/model-test/src/lib/fixture/extract.ts
12756
13741
  import { Node as Node16, Project as Project9, SyntaxKind as SyntaxKind7 } from "ts-morph";
@@ -13247,21 +14232,21 @@ function collectIdentityImports(sourceFile) {
13247
14232
  // packages/dbx-cli/model-test/src/lib/fixture/inspect.ts
13248
14233
  var FIXTURE_RELATIVE_PATH = "src/test/fixture.ts";
13249
14234
  async function inspectAppFixtures(apiAbs, apiRel) {
13250
- const absolutePath = join10(apiAbs, FIXTURE_RELATIVE_PATH);
14235
+ const absolutePath = join11(apiAbs, FIXTURE_RELATIVE_PATH);
13251
14236
  const relativePath = `${apiRel}/${FIXTURE_RELATIVE_PATH}`;
13252
- const text = await readFile11(absolutePath, "utf8");
14237
+ const text = await readFile12(absolutePath, "utf8");
13253
14238
  return extractAppFixturesFromText({ text, fixturePath: relativePath });
13254
14239
  }
13255
14240
 
13256
14241
  // packages/dbx-cli/validate/src/lib/model-list-component/extract.ts
13257
- import { readdir as readdir8, readFile as readFile12, stat as stat10 } from "node:fs/promises";
13258
- import { basename as basename5, join as join11, relative as relative6, sep as sep7 } from "node:path";
14242
+ import { readdir as readdir9, readFile as readFile13, stat as stat11 } from "node:fs/promises";
14243
+ import { basename as basename6, join as join12, relative as relative7, sep as sep7 } from "node:path";
13259
14244
  import { Node as Node17, Project as Project10 } from "ts-morph";
13260
14245
  var MODEL_SUBPATH = "src/lib/model";
13261
14246
  var MODEL_SUBPATH_PREFIX = `${MODEL_SUBPATH}/`;
13262
14247
  var RESERVED_NAMES = RESERVED_MODEL_FOLDERS.map((r) => r.name);
13263
14248
  async function extractComponentModels(componentAbs) {
13264
- const modelRoot = join11(componentAbs, MODEL_SUBPATH);
14249
+ const modelRoot = join12(componentAbs, MODEL_SUBPATH);
13265
14250
  const folders = await listFolders(modelRoot);
13266
14251
  if (folders === void 0) {
13267
14252
  const result2 = { modelRoot, models: [], skipped: [], unidentifiedFolders: [] };
@@ -13351,13 +14336,13 @@ function projectRich(model, folder) {
13351
14336
  }
13352
14337
  async function fallbackForFolder(input) {
13353
14338
  const { modelRoot, componentAbs, folder, project } = input;
13354
- const folderAbs = join11(modelRoot, folder);
14339
+ const folderAbs = join12(modelRoot, folder);
13355
14340
  const mainFile = await readMainFile(folderAbs, folder);
13356
14341
  if (mainFile === void 0) return void 0;
13357
14342
  const sourceFile = project.createSourceFile(mainFile.fileName, mainFile.text, { overwrite: true });
13358
14343
  const identity = findIdentityIn(sourceFile);
13359
14344
  if (!identity) return void 0;
13360
- const sourceFileRel = relative6(componentAbs, mainFile.absPath).split(sep7).join("/");
14345
+ const sourceFileRel = relative7(componentAbs, mainFile.absPath).split(sep7).join("/");
13361
14346
  return {
13362
14347
  folder,
13363
14348
  modelName: deriveModelName(identity.constName, folder),
@@ -13373,13 +14358,13 @@ async function listFolders(modelRoot) {
13373
14358
  let result;
13374
14359
  let isDir = false;
13375
14360
  try {
13376
- const stats = await stat10(modelRoot);
14361
+ const stats = await stat11(modelRoot);
13377
14362
  isDir = stats.isDirectory();
13378
14363
  } catch {
13379
14364
  isDir = false;
13380
14365
  }
13381
14366
  if (isDir) {
13382
- const entries = await readdir8(modelRoot, { withFileTypes: true });
14367
+ const entries = await readdir9(modelRoot, { withFileTypes: true });
13383
14368
  const out = [];
13384
14369
  for (const entry of entries) {
13385
14370
  if (entry.isDirectory()) out.push(entry.name);
@@ -13390,11 +14375,11 @@ async function listFolders(modelRoot) {
13390
14375
  return result;
13391
14376
  }
13392
14377
  async function readMainFile(folderAbs, folder) {
13393
- const candidate = join11(folderAbs, `${folder}.ts`);
14378
+ const candidate = join12(folderAbs, `${folder}.ts`);
13394
14379
  let result;
13395
14380
  try {
13396
- const text = await readFile12(candidate, "utf8");
13397
- result = { absPath: candidate, fileName: basename5(candidate), text };
14381
+ const text = await readFile13(candidate, "utf8");
14382
+ result = { absPath: candidate, fileName: basename6(candidate), text };
13398
14383
  } catch {
13399
14384
  result = void 0;
13400
14385
  }
@@ -13539,21 +14524,21 @@ __export(model_api_list_app_exports, {
13539
14524
  });
13540
14525
 
13541
14526
  // packages/dbx-cli/validate/src/lib/model-api-list-app/extract.ts
13542
- import { readFile as readFile13 } from "node:fs/promises";
13543
- import { join as join12, relative as relative7, sep as sep8 } from "node:path";
14527
+ import { readFile as readFile14 } from "node:fs/promises";
14528
+ import { join as join13, relative as relative8, sep as sep8 } from "node:path";
13544
14529
  var API_SUFFIX2 = ".api.ts";
13545
14530
  var MODEL_SUBPATH2 = "src/lib";
13546
14531
  async function extractApiList(input) {
13547
- const modelRoot = join12(input.componentAbs, MODEL_SUBPATH2);
14532
+ const modelRoot = join13(input.componentAbs, MODEL_SUBPATH2);
13548
14533
  const apiFiles = await collectFilesWithSuffix(modelRoot, API_SUFFIX2);
13549
14534
  const entries = [];
13550
14535
  const files = [];
13551
14536
  for (const filePath of apiFiles) {
13552
- const text = await readFile13(filePath, "utf8");
14537
+ const text = await readFile14(filePath, "utf8");
13553
14538
  if (!text.includes("callModelFirebaseFunctionMapFactory")) {
13554
14539
  continue;
13555
14540
  }
13556
- const sourceFileRel = relative7(input.componentAbs, filePath).split(sep8).join("/");
14541
+ const sourceFileRel = relative8(input.componentAbs, filePath).split(sep8).join("/");
13557
14542
  const extraction = extractCrudEntries({ name: sourceFileRel, text });
13558
14543
  const fileEntries = extraction.entries.map((entry) => ({ ...entry, sourceFile: sourceFileRel }));
13559
14544
  const counts = countVerbs(fileEntries);
@@ -13684,17 +14669,17 @@ __export(model_api_lookup_exports, {
13684
14669
  });
13685
14670
 
13686
14671
  // packages/dbx-cli/validate/src/lib/model-api-lookup/extract.ts
13687
- import { readFile as readFile15 } from "node:fs/promises";
13688
- import { join as join14, relative as relative9, sep as sep10 } from "node:path";
14672
+ import { readFile as readFile16 } from "node:fs/promises";
14673
+ import { join as join15, relative as relative10, sep as sep10 } from "node:path";
13689
14674
 
13690
14675
  // packages/dbx-cli/validate/src/lib/model-api-lookup/extract-actions.ts
13691
- import { readFile as readFile14 } from "node:fs/promises";
13692
- import { join as join13, relative as relative8, sep as sep9 } from "node:path";
14676
+ import { readFile as readFile15 } from "node:fs/promises";
14677
+ import { join as join14, relative as relative9, sep as sep9 } from "node:path";
13693
14678
  import { Project as Project11, SyntaxKind as SyntaxKind8 } from "ts-morph";
13694
14679
  var ACTION_SERVER_SUFFIX = ".action.server.ts";
13695
14680
  var COMMON_MODEL_SUBPATH = "src/app/common/model";
13696
14681
  async function buildActionLookup(apiAbs) {
13697
- const modelRoot = join13(apiAbs, COMMON_MODEL_SUBPATH);
14682
+ const modelRoot = join14(apiAbs, COMMON_MODEL_SUBPATH);
13698
14683
  const files = await collectFilesWithSuffix(modelRoot, ACTION_SERVER_SUFFIX);
13699
14684
  const methodsByParams = /* @__PURE__ */ new Map();
13700
14685
  const factoriesByParams = /* @__PURE__ */ new Map();
@@ -13703,9 +14688,9 @@ async function buildActionLookup(apiAbs) {
13703
14688
  }
13704
14689
  const project = new Project11({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
13705
14690
  for (const filePath of files) {
13706
- const text = await readFile14(filePath, "utf8");
14691
+ const text = await readFile15(filePath, "utf8");
13707
14692
  const sourceFile = project.createSourceFile(filePath, text, { overwrite: true });
13708
- const sourceFileRel = relative8(apiAbs, filePath).split(sep9).join("/");
14693
+ const sourceFileRel = relative9(apiAbs, filePath).split(sep9).join("/");
13709
14694
  collectAbstractMethods({ sourceFile, sourceFileRel, methodsByParams });
13710
14695
  collectFactoryFunctions({ sourceFile, sourceFileRel, factoriesByParams });
13711
14696
  }
@@ -13865,13 +14850,13 @@ function mapDocFields(fields) {
13865
14850
  return result;
13866
14851
  }
13867
14852
  async function collectApiSources(componentAbs) {
13868
- const root = join14(componentAbs, COMPONENT_LIB_SUBPATH2);
14853
+ const root = join15(componentAbs, COMPONENT_LIB_SUBPATH2);
13869
14854
  const files = await collectFilesWithSuffix(root, API_SUFFIX3);
13870
14855
  const sources = [];
13871
14856
  for (const fileAbs of files) {
13872
- const text = await readFile15(fileAbs, "utf8");
14857
+ const text = await readFile16(fileAbs, "utf8");
13873
14858
  if (!text.includes("callModelFirebaseFunctionMapFactory")) continue;
13874
- const fileRel = relative9(componentAbs, fileAbs).split(sep10).join("/");
14859
+ const fileRel = relative10(componentAbs, fileAbs).split(sep10).join("/");
13875
14860
  sources.push({ fileAbs, fileRel, text });
13876
14861
  }
13877
14862
  return sources;
@@ -14019,14 +15004,14 @@ __export(server_actions_list_app_exports, {
14019
15004
  });
14020
15005
 
14021
15006
  // packages/dbx-cli/validate/src/lib/server-actions-list-app/extract.ts
14022
- import { readdir as readdir9, readFile as readFile16 } from "node:fs/promises";
14023
- import { dirname as dirname2, join as join15, relative as relative10, sep as sep11 } from "node:path";
15007
+ import { readdir as readdir10, readFile as readFile17 } from "node:fs/promises";
15008
+ import { dirname as dirname3, join as join16, relative as relative11, sep as sep11 } from "node:path";
14024
15009
  import { Node as Node18, Project as Project12 } from "ts-morph";
14025
15010
  var ACTION_SERVER_SUFFIX2 = ".action.server.ts";
14026
15011
  var COMMON_MODEL_SUBPATH2 = "src/app/common/model";
14027
15012
  var COMMON_BARREL_REL = "src/app/common/index.ts";
14028
15013
  async function extractServerActions(apiAbs, apiRel) {
14029
- const modelRoot = join15(apiAbs, COMMON_MODEL_SUBPATH2);
15014
+ const modelRoot = join16(apiAbs, COMMON_MODEL_SUBPATH2);
14030
15015
  const actionFiles = await collectFilesWithSuffix(modelRoot, ACTION_SERVER_SUFFIX2);
14031
15016
  if (actionFiles.length === 0) {
14032
15017
  return { modelRoot, entries: [], fixtureStatus: "ok" };
@@ -14046,14 +15031,14 @@ async function extractServerActions(apiAbs, apiRel) {
14046
15031
  }
14047
15032
  const entries = [];
14048
15033
  for (const filePath of actionFiles) {
14049
- const text = await readFile16(filePath, "utf8");
15034
+ const text = await readFile17(filePath, "utf8");
14050
15035
  const sourceFile = project.createSourceFile(filePath, text, { overwrite: true });
14051
15036
  const classes = findServerActionClasses(sourceFile);
14052
15037
  for (const className of classes) {
14053
15038
  const wiring = await inspectSiblingModule(filePath, className);
14054
15039
  const expectedGetterName = camelize(className);
14055
15040
  const fixtureCoverage = fixtureStatus === "ok" ? fixtureCoverageMap.get(expectedGetterName) ?? unmatchedFixtureCoverage(expectedGetterName) : void 0;
14056
- const sourceFileRel = relative10(apiAbs, filePath).split(sep11).join("/");
15041
+ const sourceFileRel = relative11(apiAbs, filePath).split(sep11).join("/");
14057
15042
  entries.push({
14058
15043
  className,
14059
15044
  sourceFile: sourceFileRel,
@@ -14095,14 +15080,14 @@ async function inspectSiblingModule(actionFilePath, className) {
14095
15080
  return { modulePath: moduleCandidates[0], providedByModule: false, exportedByModule: false };
14096
15081
  }
14097
15082
  async function findModuleCandidates(actionFilePath) {
14098
- const dir = dirname2(actionFilePath);
15083
+ const dir = dirname3(actionFilePath);
14099
15084
  const out = [];
14100
15085
  try {
14101
- const entries = await readdir9(dir, { withFileTypes: true });
15086
+ const entries = await readdir10(dir, { withFileTypes: true });
14102
15087
  for (const entry of entries) {
14103
15088
  if (!entry.isFile()) continue;
14104
15089
  if (!entry.name.endsWith(".module.ts")) continue;
14105
- out.push(join15(dir, entry.name));
15090
+ out.push(join16(dir, entry.name));
14106
15091
  }
14107
15092
  } catch {
14108
15093
  }
@@ -14112,7 +15097,7 @@ async function findModuleCandidates(actionFilePath) {
14112
15097
  async function safeRead(path) {
14113
15098
  let result;
14114
15099
  try {
14115
- result = await readFile16(path, "utf8");
15100
+ result = await readFile17(path, "utf8");
14116
15101
  } catch {
14117
15102
  result = void 0;
14118
15103
  }
@@ -14159,7 +15144,7 @@ function moduleArrayContains(decoratorArg, propertyName, className) {
14159
15144
  return false;
14160
15145
  }
14161
15146
  async function readCommonBarrelExports(apiAbs) {
14162
- const path = join15(apiAbs, COMMON_BARREL_REL);
15147
+ const path = join16(apiAbs, COMMON_BARREL_REL);
14163
15148
  const text = await safeRead(path);
14164
15149
  if (text === void 0) return /* @__PURE__ */ new Set();
14165
15150
  const project = new Project12({ useInMemoryFileSystem: true });
@@ -14338,8 +15323,8 @@ function formatReportAsJson4(report) {
14338
15323
 
14339
15324
  // packages/dbx-cli/validate/src/lib/dbx-asset-list-app/format.markdown.ts
14340
15325
  function formatReportAsMarkdown4(report) {
14341
- const basename6 = report.componentDir.split("/").pop() ?? report.componentDir;
14342
- const lines = [`# App assets \u2014 ${basename6}`, "", `Component: \`${report.componentDir}\``, `App: \`${report.apiDir}\``, "", `\`src/lib/assets.ts\`: ${formatBool(report.assetsFileExists)}`, `Barrel re-exports \`./assets\`: ${formatBool(report.barrelReExportsAssets)}`, `\`provideDbxAssetLoader()\` called in \`src/root.app.config.ts\`: ${formatBool(report.providerWiredInApp)}`, "", `## Assets (${report.assets.length})`];
15326
+ const basename7 = report.componentDir.split("/").pop() ?? report.componentDir;
15327
+ const lines = [`# App assets \u2014 ${basename7}`, "", `Component: \`${report.componentDir}\``, `App: \`${report.apiDir}\``, "", `\`src/lib/assets.ts\`: ${formatBool(report.assetsFileExists)}`, `Barrel re-exports \`./assets\`: ${formatBool(report.barrelReExportsAssets)}`, `\`provideDbxAssetLoader()\` called in \`src/root.app.config.ts\`: ${formatBool(report.providerWiredInApp)}`, "", `## Assets (${report.assets.length})`];
14343
15328
  if (report.assets.length === 0) {
14344
15329
  lines.push("", "_None found._");
14345
15330
  } else {
@@ -14527,11 +15512,11 @@ function formatReportAsJson5(report) {
14527
15512
 
14528
15513
  // packages/dbx-cli/validate/src/lib/notification-m-list-app/format.markdown.ts
14529
15514
  function formatReportAsMarkdown5(report) {
14530
- const basename6 = report.componentDir.split("/").pop() ?? report.componentDir;
15515
+ const basename7 = report.componentDir.split("/").pop() ?? report.componentDir;
14531
15516
  const aggregatorRecordText = report.aggregatorRecordName ? code(report.aggregatorRecordName) : "_Not defined._";
14532
15517
  const templateConfigsFactoryText = report.templateConfigsArrayFactoryName ? code(report.templateConfigsArrayFactoryName) : "_Not defined._";
14533
15518
  const lines = [
14534
- `# App notifications \u2014 ${basename6}`,
15519
+ `# App notifications \u2014 ${basename7}`,
14535
15520
  "",
14536
15521
  `Component: \`${report.componentDir}\``,
14537
15522
  `API: \`${report.apiDir}\``,
@@ -14728,9 +15713,9 @@ function formatReportAsJson6(report) {
14728
15713
 
14729
15714
  // packages/dbx-cli/validate/src/lib/storagefile-m-list-app/format.markdown.ts
14730
15715
  function formatReportAsMarkdown6(report) {
14731
- const basename6 = report.componentDir.split("/").pop() ?? report.componentDir;
15716
+ const basename7 = report.componentDir.split("/").pop() ?? report.componentDir;
14732
15717
  const uploadServiceFactoryText = report.uploadServiceFactoryName ? code2(report.uploadServiceFactoryName) : "_Not defined._";
14733
- const lines = [`# App storagefiles \u2014 ${basename6}`, "", `Component: \`${report.componentDir}\``, `API: \`${report.apiDir}\``, "", `Upload service factory: ${uploadServiceFactoryText}`, `Wired via \`StorageFileInitializeFromUploadService\` provider: ${formatBool3(report.uploadServiceWiredInApi)}`, `Processing handler call present: ${formatBool3(report.processingHandlerWiredInApi)}`, "", `## Purposes (${report.purposes.length})`];
15718
+ const lines = [`# App storagefiles \u2014 ${basename7}`, "", `Component: \`${report.componentDir}\``, `API: \`${report.apiDir}\``, "", `Upload service factory: ${uploadServiceFactoryText}`, `Wired via \`StorageFileInitializeFromUploadService\` provider: ${formatBool3(report.uploadServiceWiredInApi)}`, `Processing handler call present: ${formatBool3(report.processingHandlerWiredInApi)}`, "", `## Purposes (${report.purposes.length})`];
14734
15719
  if (report.purposes.length === 0) {
14735
15720
  lines.push("", "_None found._");
14736
15721
  } else {
@@ -14914,8 +15899,8 @@ __export(dbx_color_template_list_app_exports, {
14914
15899
  });
14915
15900
 
14916
15901
  // packages/dbx-cli/validate/src/lib/dbx-color-template-list-app/inspect.ts
14917
- import { readFile as readFile17 } from "node:fs/promises";
14918
- import { join as join16, relative as relative11, sep as sep12 } from "node:path";
15902
+ import { readFile as readFile18 } from "node:fs/promises";
15903
+ import { join as join17, relative as relative12, sep as sep12 } from "node:path";
14919
15904
  var CANDIDATE_RELPATHS = ["src/root.app.config.ts", "src/app.config.ts", "src/app.module.ts", "src/app/app.config.ts", "src/app/app.module.ts"];
14920
15905
  async function inspectColorTemplates(apiAbs, apiRel) {
14921
15906
  const present = [];
@@ -14925,7 +15910,7 @@ async function inspectColorTemplates(apiAbs, apiRel) {
14925
15910
  for (const candidate of candidates) {
14926
15911
  const text = await readFileIfExists2(candidate);
14927
15912
  if (text === void 0) continue;
14928
- const rel = relative11(apiAbs, candidate).split(sep12).join("/");
15913
+ const rel = relative12(apiAbs, candidate).split(sep12).join("/");
14929
15914
  present.push({ relPath: rel, text });
14930
15915
  }
14931
15916
  } catch (err) {
@@ -14942,25 +15927,25 @@ async function inspectColorTemplates(apiAbs, apiRel) {
14942
15927
  async function collectCandidatePaths(apiAbs) {
14943
15928
  const explicit = [];
14944
15929
  for (const rel of CANDIDATE_RELPATHS) {
14945
- explicit.push(join16(apiAbs, rel));
15930
+ explicit.push(join17(apiAbs, rel));
14946
15931
  }
14947
- const scanned = await scanProviderFiles(join16(apiAbs, "src"));
15932
+ const scanned = await scanProviderFiles(join17(apiAbs, "src"));
14948
15933
  return [...explicit, ...scanned];
14949
15934
  }
14950
15935
  async function scanProviderFiles(srcAbs) {
14951
- const { readdir: readdir10 } = await import("node:fs/promises");
15936
+ const { readdir: readdir11 } = await import("node:fs/promises");
14952
15937
  const out = [];
14953
15938
  const stack = [srcAbs];
14954
15939
  while (stack.length > 0) {
14955
15940
  const current = stack.pop();
14956
15941
  let entries;
14957
15942
  try {
14958
- entries = await readdir10(current, { withFileTypes: true });
15943
+ entries = await readdir11(current, { withFileTypes: true });
14959
15944
  } catch {
14960
15945
  continue;
14961
15946
  }
14962
15947
  for (const entry of entries) {
14963
- const full = join16(current, entry.name);
15948
+ const full = join17(current, entry.name);
14964
15949
  if (entry.isDirectory()) {
14965
15950
  stack.push(full);
14966
15951
  continue;
@@ -14975,7 +15960,7 @@ async function scanProviderFiles(srcAbs) {
14975
15960
  async function readFileIfExists2(absPath) {
14976
15961
  let result;
14977
15962
  try {
14978
- result = await readFile17(absPath, "utf8");
15963
+ result = await readFile18(absPath, "utf8");
14979
15964
  } catch (err) {
14980
15965
  const code3 = err.code;
14981
15966
  if (code3 !== "ENOENT" && code3 !== "ENOTDIR" && code3 !== "EISDIR") {
@@ -15679,11 +16664,12 @@ function matchKindForRadius(exact, raw) {
15679
16664
  if (exact.defaults.light === raw || exact.defaults.dark === raw) return "(exact-match design-system corner token).";
15680
16665
  return "(idiomatic full / pill radius \u2014 design system token).";
15681
16666
  }
15682
- function findExactRoleToken(registry, raw, role) {
16667
+ function findExactRoleToken(params) {
16668
+ const { registry, raw, role, isAllowed } = params;
15683
16669
  const candidates = registry.byRole.get(role) ?? [];
15684
16670
  let result;
15685
16671
  for (const entry of candidates) {
15686
- if (entry.defaults.light === raw || entry.defaults.dark === raw) {
16672
+ if ((entry.defaults.light === raw || entry.defaults.dark === raw) && (isAllowed === void 0 || isAllowed(entry))) {
15687
16673
  result = entry;
15688
16674
  break;
15689
16675
  }
@@ -15828,7 +16814,7 @@ var hardcodedRadius = (input) => {
15828
16814
  const colonIndex = input.scss.indexOf(":", match.index);
15829
16815
  if (colonIndex !== -1 && isInsideScssVarDeclaration(input.scss, colonIndex)) continue;
15830
16816
  const raw = match[1];
15831
- let exact = findExactRoleToken(input.tokenRegistry, raw, "radius");
16817
+ let exact = findExactRoleToken({ registry: input.tokenRegistry, raw, role: "radius" });
15832
16818
  if (exact === void 0 && FULL_RADIUS_SYNONYMS.has(raw)) {
15833
16819
  exact = input.tokenRegistry.findByCssVariable("--mat-sys-corner-full");
15834
16820
  }
@@ -15859,7 +16845,7 @@ var hardcodedShadow = (input) => {
15859
16845
  if (match.index === void 0) continue;
15860
16846
  const raw = match[1].trim();
15861
16847
  if (raw.startsWith("var(") || raw === "none") continue;
15862
- const exact = findExactRoleToken(input.tokenRegistry, raw, "elevation");
16848
+ const exact = findExactRoleToken({ registry: input.tokenRegistry, raw, role: "elevation" });
15863
16849
  const fix = exact === void 0 ? 'Hardcoded `box-shadow:` declarations should map to one of the `--mat-sys-level0..5` elevation tokens, or wrap the surface in `<dbx-content-box [elevate]="true">` so the shadow comes from the design system.' : `Replace this hardcoded shadow with \`box-shadow: var(${exact.cssVariable})\` or use \`<dbx-content-box [elevate]="true">\` to apply the elevation through the wrapper component.`;
15864
16850
  matches.push({
15865
16851
  id: "hardcoded-shadow",
@@ -15904,12 +16890,13 @@ var hardcodedHintColor = (input) => {
15904
16890
  return matches;
15905
16891
  };
15906
16892
  var HARDCODED_PADDING_RE = /(?:padding|margin)\s*(?:-(?:top|right|bottom|left))?\s*:\s*(\d+(?:\.\d+)?px)(?!\s*\))/gi;
16893
+ var DBX_PADDING_SCALE_RE = /^--dbx-padding-\d+$/;
15907
16894
  var hardcodedPadding = (input) => {
15908
16895
  const matches = [];
15909
16896
  for (const match of input.scss.matchAll(HARDCODED_PADDING_RE)) {
15910
16897
  if (match.index === void 0) continue;
15911
16898
  const raw = match[1];
15912
- const exact = findExactRoleToken(input.tokenRegistry, raw, "spacing");
16899
+ const exact = findExactRoleToken({ registry: input.tokenRegistry, raw, role: "spacing", isAllowed: (entry) => DBX_PADDING_SCALE_RE.test(entry.cssVariable) });
15913
16900
  if (exact !== void 0) {
15914
16901
  const utility = exact.utilityClasses?.[0];
15915
16902
  const fix = ["Replace `" + match[0].trim() + "` with `var(" + exact.cssVariable + ")`" + (utility === void 0 ? "" : ` (or use the \`${utility}\` utility class on the host element)`) + "."].join("\n");
@@ -16647,5 +17634,6 @@ export {
16647
17634
  storagefile_m_validate_folder_exports as storagefileValidateFolder,
16648
17635
  system_m_list_app_exports as systemListApp,
16649
17636
  system_m_validate_folder_exports as systemValidateFolder,
16650
- ui_smell_check_exports as uiSmellCheck
17637
+ ui_smell_check_exports as uiSmellCheck,
17638
+ workspace_validate_exports as workspaceValidate
16651
17639
  };