@cyberxon/xon 0.3.1 → 0.4.1

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/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ import { collectSourceFiles, writeSourceFiles } from "./source.js";
12
12
  import { displayOrg, loginAccessToken, loginCredentials, loginJwt, loginSfdxUrl, loginWeb, logoutOrg, openInBrowser, openOrg } from "./org.js";
13
13
  import { clearOrgs, listOrgs, tryGetOrg } from "./org-store.js";
14
14
  import { applyFlagsDir } from "./flags-dir.js";
15
+ import { formatMetadataFilter, parseMetadataFilter } from "./metadata-registry.js";
15
16
  const cliVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
16
17
  const program = new Command();
17
18
  function output(value) {
@@ -52,6 +53,20 @@ async function resolveTargetOrg(explicit) {
52
53
  const project = await tryReadProject(".");
53
54
  return project?.defaultOrg;
54
55
  }
56
+ async function resolveSourceDirs(explicit) {
57
+ if (explicit.length > 0) {
58
+ return explicit;
59
+ }
60
+ const project = await tryReadProject(".");
61
+ return [project?.sourceDir ?? "src"];
62
+ }
63
+ async function resolveSourceDir(explicit) {
64
+ if (explicit) {
65
+ return explicit;
66
+ }
67
+ const project = await tryReadProject(".");
68
+ return project?.sourceDir ?? "src";
69
+ }
55
70
  async function saveProjectLoginDefaults(baseUrl, orgAlias) {
56
71
  const project = await tryReadProject(".");
57
72
  if (!project) {
@@ -75,6 +90,7 @@ const parseWait = (value) => {
75
90
  };
76
91
  const DEFAULT_DEPLOY_WAIT_SECONDS = 33 * 60;
77
92
  const collect = (value, previous) => previous.concat([value]);
93
+ const collectMetadata = (value, previous) => previous.concat([formatMetadataFilter(parseMetadataFilter(value))]);
78
94
  function parseEnum(name, allowed) {
79
95
  return (value) => {
80
96
  if (!allowed.includes(value)) {
@@ -119,7 +135,7 @@ function addDeployStartFlags(cmd) {
119
135
  .option("--json", "Format output as JSON", false)
120
136
  .option("--junit", "Output JUnit test results", false)
121
137
  .option("-x, --manifest <path>", "Full file path for manifest (package.xml) of components to deploy")
122
- .option("-m, --metadata <name>", "Metadata component names to deploy (repeatable)", collect, [])
138
+ .option("-m, --metadata <name>", "Metadata component names to deploy, e.g. ApexClass or ApexClass:MyClass (repeatable)", collectMetadata, [])
123
139
  .option("--metadata-dir <path>", "Root of directory of metadata formatted files to deploy")
124
140
  .option("--post-destructive-changes <path>", "File path for a manifest of components to delete after the deploy")
125
141
  .option("--pre-destructive-changes <path>", "File path for a manifest of components to delete before the deploy")
@@ -145,8 +161,9 @@ project
145
161
  .command("create <name>")
146
162
  .description("Create a new Xon project")
147
163
  .option("-d, --directory <path>", "Directory to create the project in", ".")
164
+ .option("-f, --full", "Scaffold the full force-app/main/default layout instead of a plain src/ folder", false)
148
165
  .action(async (name, options) => {
149
- const root = await createProject(options.directory, name);
166
+ const root = await createProject(options.directory, name, { full: options.full });
150
167
  console.log(`Created project ${name} in ${root}`);
151
168
  });
152
169
  const projectDeploy = project.command("deploy").description("Deploy metadata between your project and an org");
@@ -160,7 +177,7 @@ addDeployStartFlags(projectDeploy.command("start").description("Deploy metadata
160
177
  options = command.opts();
161
178
  }
162
179
  const client = await apiClient(options);
163
- const sourceDirs = (options.sourceDir.length > 0 ? options.sourceDir : ["src"]).map((dir) => path.resolve(dir));
180
+ const sourceDirs = (await resolveSourceDirs(options.sourceDir)).map((dir) => path.resolve(dir));
164
181
  const status = await startDeploy(client, sourceDirs, {
165
182
  targetOrg: await resolveTargetOrg(options.targetOrg),
166
183
  checkOnly: options.dryRun,
@@ -180,6 +197,7 @@ addDeployStartFlags(projectDeploy.command("start").description("Deploy metadata
180
197
  coverageFormatters: options.coverageFormatters && options.coverageFormatters.length > 0 ? options.coverageFormatters : undefined,
181
198
  junit: options.junit,
182
199
  });
200
+ console.log(`Deploy Id: ${status.id}`);
183
201
  const waitSeconds = options.async ? 0 : options.wait;
184
202
  const final = await waitForDeploy(client, status, waitSeconds);
185
203
  await writeDeployResults(options.resultsDir, final);
@@ -193,7 +211,7 @@ addDeployStartFlags(projectDeploy.command("validate").description("Validate a me
193
211
  options = command.opts();
194
212
  }
195
213
  const client = await apiClient(options);
196
- const sourceDirs = (options.sourceDir.length > 0 ? options.sourceDir : ["src"]).map((dir) => path.resolve(dir));
214
+ const sourceDirs = (await resolveSourceDirs(options.sourceDir)).map((dir) => path.resolve(dir));
197
215
  const status = await startDeploy(client, sourceDirs, {
198
216
  targetOrg: await resolveTargetOrg(options.targetOrg),
199
217
  checkOnly: true,
@@ -213,6 +231,7 @@ addDeployStartFlags(projectDeploy.command("validate").description("Validate a me
213
231
  coverageFormatters: options.coverageFormatters && options.coverageFormatters.length > 0 ? options.coverageFormatters : undefined,
214
232
  junit: options.junit,
215
233
  });
234
+ console.log(`Deploy Id: ${status.id}`);
216
235
  const final = await waitForDeploy(client, status, options.wait);
217
236
  await writeDeployResults(options.resultsDir, final);
218
237
  printDeployResult(final, options);
@@ -260,10 +279,11 @@ projectDeploy
260
279
  projectDeploy
261
280
  .command("preview")
262
281
  .description("Preview deployment details for the local project source")
263
- .option("-d, --source-dir <dir>", "Directory of source to preview", "src")
282
+ .option("-d, --source-dir <dir>", "Directory of source to preview")
264
283
  .action(async (options) => {
265
284
  await requireProjectRoot();
266
- const files = await collectSourceFiles(path.resolve(options.sourceDir));
285
+ const sourceDir = await resolveSourceDir(options.sourceDir);
286
+ const files = await collectSourceFiles(path.resolve(sourceDir));
267
287
  console.table(previewDeploy(files));
268
288
  });
269
289
  const projectRetrieve = project.command("retrieve").description("Retrieve metadata from an org into your project");
@@ -275,8 +295,8 @@ projectRetrieve
275
295
  .option("-c, --ignore-conflicts", "Ignore conflicts and save files, even if they overwrite local changes", false)
276
296
  .option("--json", "Format output as JSON", false)
277
297
  .option("-x, --manifest <path>", "File path for the manifest (package.xml) that specifies the components to retrieve")
278
- .option("-m, --metadata <name>", "Metadata component names to retrieve (repeatable)", collect, [])
279
- .option("-r, --output-dir <path>", "Directory root for the retrieved source files", "src")
298
+ .option("-m, --metadata <name>", "Metadata component names to retrieve, e.g. ApexClass or ApexClass:MyClass (repeatable)", collectMetadata, [])
299
+ .option("-r, --output-dir <path>", "Directory root for the retrieved source files")
280
300
  .option("-n, --package-name <name>", "Package names to retrieve (repeatable)", collect, [])
281
301
  .option("--single-package", "Indicates that the zip file points to a directory structure for a single package", false)
282
302
  .option("-d, --source-dir <path>", "File paths for source to retrieve from the org (repeatable)", collect, [])
@@ -309,8 +329,9 @@ projectRetrieve
309
329
  result = await resumeRetrieve(client, result.id, options.wait);
310
330
  }
311
331
  if (result.success && result.files && (options.unzip || !options.targetMetadataDir)) {
312
- await writeSourceFiles(path.resolve(options.outputDir), result.files);
313
- console.log(`Retrieved ${result.files.length} file(s) into ${options.outputDir}`);
332
+ const outputDir = await resolveSourceDir(options.outputDir);
333
+ await writeSourceFiles(path.resolve(outputDir), result.files);
334
+ console.log(`Retrieved ${result.files.length} file(s) into ${outputDir}`);
314
335
  }
315
336
  if (result.success && result.metadataZip && options.targetMetadataDir) {
316
337
  const zipDir = path.resolve(options.targetMetadataDir);
package/dist/deploy.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { findTypeBySuffix } from "./metadata-registry.js";
2
4
  import { pollJob } from "./polling.js";
3
5
  import { collectSourceFiles } from "./source.js";
4
6
  export const TEST_LEVELS = ["NoTestRun", "RunSpecifiedTests", "RunLocalTests", "RunAllTestsInOrg", "RunRelevantTests"];
@@ -54,6 +56,19 @@ export async function cancelDeploy(client, id) {
54
56
  export async function quickDeploy(client, id) {
55
57
  return client.post(`/deployRequests/${encodeURIComponent(id)}/quickDeploy`, undefined);
56
58
  }
59
+ function inferSuffix(filePath) {
60
+ const base = path.basename(filePath);
61
+ const metaMatch = base.match(/\.([A-Za-z0-9]+)-meta\.xml$/);
62
+ if (metaMatch) {
63
+ return metaMatch[1];
64
+ }
65
+ const dotIndex = base.lastIndexOf(".");
66
+ return dotIndex === -1 ? undefined : base.slice(dotIndex + 1);
67
+ }
57
68
  export function previewDeploy(files) {
58
- return files.map((file) => ({ path: file.path, action: "Add" }));
69
+ return files.map((file) => {
70
+ const suffix = inferSuffix(file.path);
71
+ const type = suffix ? findTypeBySuffix(suffix)?.name : undefined;
72
+ return { path: file.path, type: type ?? "Unknown", action: "Add" };
73
+ });
59
74
  }
@@ -0,0 +1,54 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function packageRoot() {
5
+ const dist = path.dirname(fileURLToPath(import.meta.url));
6
+ return path.dirname(dist);
7
+ }
8
+ let cached;
9
+ function load() {
10
+ if (cached) {
11
+ return cached;
12
+ }
13
+ const registryPath = path.join(packageRoot(), "registry", "metadataRegistry.json");
14
+ const raw = JSON.parse(readFileSync(registryPath, "utf8"));
15
+ const byName = new Map();
16
+ const byId = new Map();
17
+ for (const def of Object.values(raw.types)) {
18
+ byName.set(def.name.toLowerCase(), def);
19
+ byId.set(def.id, def);
20
+ if (def.children?.types) {
21
+ for (const childDef of Object.values(def.children.types)) {
22
+ const child = { ...childDef, parentId: def.id };
23
+ byName.set(child.name.toLowerCase(), child);
24
+ byId.set(child.id, child);
25
+ }
26
+ }
27
+ }
28
+ cached = { raw, byName, byId };
29
+ return cached;
30
+ }
31
+ export function findTypeByName(name) {
32
+ return load().byName.get(name.trim().toLowerCase());
33
+ }
34
+ export function findTypeBySuffix(suffix) {
35
+ const { raw, byId } = load();
36
+ const typeId = raw.suffixes[suffix] ?? raw.suffixes[suffix.toLowerCase()];
37
+ return typeId ? byId.get(typeId) : undefined;
38
+ }
39
+ export function parseMetadataFilter(raw) {
40
+ const separatorIndex = raw.indexOf(":");
41
+ const typePart = (separatorIndex === -1 ? raw : raw.slice(0, separatorIndex)).trim();
42
+ const namePart = separatorIndex === -1 ? undefined : raw.slice(separatorIndex + 1).trim();
43
+ const def = findTypeByName(typePart);
44
+ if (!def) {
45
+ throw new Error(`Unknown metadata type '${typePart}'. Check the Metadata API name (e.g. ApexClass, CustomObject, Flow).`);
46
+ }
47
+ if (namePart !== undefined && namePart.length === 0) {
48
+ throw new Error(`Invalid --metadata value '${raw}': expected '${def.name}' or '${def.name}:<name>'.`);
49
+ }
50
+ return { type: def.name, name: namePart };
51
+ }
52
+ export function formatMetadataFilter(filter) {
53
+ return filter.name ? `${filter.type}:${filter.name}` : filter.type;
54
+ }
package/dist/project.js CHANGED
@@ -1,15 +1,38 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  const projectFile = ".xon/project.json";
4
- export async function createProject(directory, name) {
4
+ const DEFAULT_SOURCE_DIR = "src";
5
+ const FULL_SOURCE_DIR = "force-app/main/default";
6
+ const FULL_METADATA_DIRS = [
7
+ "applications",
8
+ "aura",
9
+ "classes",
10
+ "contentassets",
11
+ "flexipages",
12
+ "layouts",
13
+ "lwc",
14
+ "objects",
15
+ "permissionsets",
16
+ "staticresources",
17
+ "tabs",
18
+ "triggers",
19
+ ];
20
+ export async function createProject(directory, name, options = {}) {
5
21
  const root = path.resolve(directory, name);
22
+ const sourceDir = options.full ? FULL_SOURCE_DIR : DEFAULT_SOURCE_DIR;
6
23
  const config = {
7
24
  name,
8
25
  apiVersion: "v1",
9
26
  createdAt: new Date().toISOString(),
27
+ sourceDir,
10
28
  };
11
29
  await mkdir(path.join(root, ".xon"), { recursive: true });
12
- await mkdir(path.join(root, "src"), { recursive: true });
30
+ if (options.full) {
31
+ await Promise.all(FULL_METADATA_DIRS.map((dir) => mkdir(path.join(root, sourceDir, dir), { recursive: true })));
32
+ }
33
+ else {
34
+ await mkdir(path.join(root, sourceDir), { recursive: true });
35
+ }
13
36
  await writeFile(path.join(root, projectFile), `${JSON.stringify(config, null, 2)}\n`, { flag: "wx" });
14
37
  return root;
15
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyberxon/xon",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "A CLI for Xon projects and deployments",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -16,7 +16,8 @@
16
16
  "node": ">=18"
17
17
  },
18
18
  "files": [
19
- "dist"
19
+ "dist",
20
+ "registry"
20
21
  ],
21
22
  "bin": {
22
23
  "xon": "dist/cli.js"