attio 0.0.1-experimental.20250825 → 0.0.1-experimental.20250829.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.
@@ -1,79 +1,76 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
- import { complete, errored } from "@attio/fetchable-npm";
4
- import { findSurfaceExports } from "../../util/find-surface-exports/find-surface-exports.js";
3
+ import { Project } from "ts-morph";
4
+ import { complete, errored, fromPromise, isComplete } from "@attio/fetchable-npm";
5
+ import { getAppEntryPoint } from "../../util/get-app-entry-point.js";
5
6
  const ASSET_FILE_EXTENSIONS = ["png"];
6
7
  export async function generateClientEntry({ srcDirAbsolute, assetsDirAbsolute, }) {
7
- let surfaceExports;
8
- try {
9
- surfaceExports = await findSurfaceExports(srcDirAbsolute);
10
- }
11
- catch (error) {
8
+ const appEntryPoint = await getAppEntryPoint(srcDirAbsolute);
9
+ if (!appEntryPoint) {
12
10
  return errored({
13
- code: "ERROR_FINDING_SURFACE_EXPORTS",
14
- error,
11
+ code: "APP_ENTRY_POINT_NOT_FOUND",
15
12
  });
16
13
  }
17
- let assetFiles;
18
- try {
19
- assetFiles = await fs.readdir(assetsDirAbsolute, { recursive: true });
20
- }
21
- catch {
22
- assetFiles = [];
14
+ const tsProject = new Project({
15
+ useInMemoryFileSystem: true,
16
+ });
17
+ const appSourceFile = tsProject.createSourceFile(appEntryPoint.path, appEntryPoint.content);
18
+ const exported = appSourceFile.getExportedDeclarations();
19
+ const hasAppExport = exported.has("app");
20
+ if (!hasAppExport) {
21
+ return errored({
22
+ code: "APP_EXPORT_NOT_FOUND",
23
+ path: appEntryPoint.path,
24
+ });
23
25
  }
26
+ const assetFilesResult = await fromPromise(fs.readdir(assetsDirAbsolute, { recursive: true }));
27
+ const assetFiles = isComplete(assetFilesResult) ? assetFilesResult.value : [];
24
28
  const assets = assetFiles
25
29
  .filter((relativeAssetPath) => ASSET_FILE_EXTENSIONS.some((extension) => relativeAssetPath.endsWith("." + extension)))
26
30
  .map((relativeAssetPath) => ({
27
31
  path: path.join(assetsDirAbsolute, relativeAssetPath),
28
32
  name: relativeAssetPath,
29
33
  }));
30
- const importSurfacesJS = surfaceExports
31
- .map(([filePath, actionKinds], index) => `import {${actionKinds
32
- .map((actionKind) => `${actionKind} as ${actionKind}${index}`)
33
- .join(", ")}} from ${JSON.stringify(filePath)};`)
34
- .join("\n");
35
- const surfaceImportNamesBySurfaceType = {
36
- recordAction: [],
37
- bulkRecordAction: [],
38
- recordWidget: [],
39
- callRecordingInsightTextSelectionAction: [],
40
- callRecordingSummaryTextSelectionAction: [],
41
- callRecordingTranscriptTextSelectionAction: [],
42
- workflowBlock: [],
43
- };
44
- surfaceExports.forEach(([, surfaceNames], index) => {
45
- surfaceNames.forEach((surfaceName) => {
46
- surfaceImportNamesBySurfaceType[surfaceName].push(`${surfaceName}${index}`);
47
- });
48
- });
49
- const registerSurfacesJS = `registerSurfaces({
50
- "record-action": [${surfaceImportNamesBySurfaceType.recordAction.join(", ")}],
51
- "bulk-record-action": [${surfaceImportNamesBySurfaceType.bulkRecordAction.join(", ")}],
52
- "record-widget": [${surfaceImportNamesBySurfaceType.recordWidget.join(", ")}],
53
- "call-recording-insight-text-selection-action": [${surfaceImportNamesBySurfaceType.callRecordingInsightTextSelectionAction.join(", ")}],
54
- "call-recording-summary-text-selection-action": [${surfaceImportNamesBySurfaceType.callRecordingSummaryTextSelectionAction.join(", ")}],
55
- "call-recording-transcript-text-selection-action": [${surfaceImportNamesBySurfaceType.callRecordingTranscriptTextSelectionAction.join(", ")}],
56
- "workflow-block": [${surfaceImportNamesBySurfaceType.workflowBlock.join(", ")}]
57
- });`;
34
+ const importAppJS = `
35
+ import {app} from ${JSON.stringify(appEntryPoint.path)}
36
+ `;
37
+ const registerSurfacesJS = `
38
+ const recordActions = app?.record?.actions
39
+ const bulkRecordActions = app?.record?.bulkActions
40
+ const recordWidgets = app?.record?.widgets
41
+ const callRecordingInsights = app?.callRecording?.insight?.textActions
42
+ const callRecordingSummaries = app?.callRecording?.summary?.textActions
43
+ const callRecordingTranscripts = app?.callRecording?.transcript?.textActions
44
+ const workflowBlocks = app?.workflow?.blocks
45
+
46
+ registerSurfaces({
47
+ "record-action": Array.isArray(recordActions) ? recordActions : [],
48
+ "bulk-record-action": Array.isArray(bulkRecordActions) ? bulkRecordActions : [],
49
+ "record-widget": Array.isArray(recordWidgets) ? recordWidgets : [],
50
+ "call-recording-insight-text-selection-action": Array.isArray(callRecordingInsights) ? callRecordingInsights : [],
51
+ "call-recording-summary-text-selection-action": Array.isArray(callRecordingSummaries) ? callRecordingSummaries : [],
52
+ "call-recording-transcript-text-selection-action": Array.isArray(callRecordingTranscripts) ? callRecordingTranscripts : [],
53
+ "workflow-block": Array.isArray(workflowBlocks) ? workflowBlocks : [],
54
+ })
55
+ `;
58
56
  const importAssetsJS = assets
59
57
  .map((asset, index) => `import A${index} from ${JSON.stringify(asset.path)};`)
60
58
  .join("\n");
61
59
  const registerAssetsJS = `
62
- const assets = [];
60
+ const assets = [];
63
61
 
64
- ${assets
62
+ ${assets
65
63
  .map((asset, index) => `assets.push({name: ${JSON.stringify(asset.name)}, data: A${index}});`)
66
64
  .join("\n")}
67
65
 
68
- registerAssets(assets);
69
- `;
66
+ registerAssets(assets);
67
+ `;
70
68
  return complete(`
71
- ${importSurfacesJS}
72
-
73
- ${importAssetsJS}
69
+ ${importAppJS}
70
+ ${importAssetsJS}
74
71
 
75
- ${registerSurfacesJS}
72
+ ${registerSurfacesJS}
76
73
 
77
- ${registerAssetsJS}
78
- `);
74
+ ${registerAssetsJS}
75
+ `);
79
76
  }
@@ -0,0 +1,79 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { complete, errored } from "@attio/fetchable-npm";
4
+ import { findSurfaceExports } from "../../util/find-surface-exports/find-surface-exports.js";
5
+ const ASSET_FILE_EXTENSIONS = ["png"];
6
+ export async function legacyGenerateClientEntry({ srcDirAbsolute, assetsDirAbsolute, }) {
7
+ let surfaceExports;
8
+ try {
9
+ surfaceExports = await findSurfaceExports(srcDirAbsolute);
10
+ }
11
+ catch (error) {
12
+ return errored({
13
+ code: "ERROR_FINDING_SURFACE_EXPORTS",
14
+ error,
15
+ });
16
+ }
17
+ let assetFiles;
18
+ try {
19
+ assetFiles = await fs.readdir(assetsDirAbsolute, { recursive: true });
20
+ }
21
+ catch {
22
+ assetFiles = [];
23
+ }
24
+ const assets = assetFiles
25
+ .filter((relativeAssetPath) => ASSET_FILE_EXTENSIONS.some((extension) => relativeAssetPath.endsWith("." + extension)))
26
+ .map((relativeAssetPath) => ({
27
+ path: path.join(assetsDirAbsolute, relativeAssetPath),
28
+ name: relativeAssetPath,
29
+ }));
30
+ const importSurfacesJS = surfaceExports
31
+ .map(([filePath, surfaces], index) => `import {${surfaces
32
+ .map((surface) => `${surface.surfaceType} as ${surface.surfaceType}${index}`)
33
+ .join(", ")}} from ${JSON.stringify(filePath)};`)
34
+ .join("\n");
35
+ const surfaceImportNamesBySurfaceType = {
36
+ recordAction: [],
37
+ bulkRecordAction: [],
38
+ recordWidget: [],
39
+ callRecordingInsightTextSelectionAction: [],
40
+ callRecordingSummaryTextSelectionAction: [],
41
+ callRecordingTranscriptTextSelectionAction: [],
42
+ workflowBlock: [],
43
+ };
44
+ surfaceExports.forEach(([, surfaces], index) => {
45
+ surfaces.forEach((surface) => {
46
+ surfaceImportNamesBySurfaceType[surface.surfaceType].push(`${surface.surfaceType}${index}`);
47
+ });
48
+ });
49
+ const registerSurfacesJS = `registerSurfaces({
50
+ "record-action": [${surfaceImportNamesBySurfaceType.recordAction.join(", ")}],
51
+ "bulk-record-action": [${surfaceImportNamesBySurfaceType.bulkRecordAction.join(", ")}],
52
+ "record-widget": [${surfaceImportNamesBySurfaceType.recordWidget.join(", ")}],
53
+ "call-recording-insight-text-selection-action": [${surfaceImportNamesBySurfaceType.callRecordingInsightTextSelectionAction.join(", ")}],
54
+ "call-recording-summary-text-selection-action": [${surfaceImportNamesBySurfaceType.callRecordingSummaryTextSelectionAction.join(", ")}],
55
+ "call-recording-transcript-text-selection-action": [${surfaceImportNamesBySurfaceType.callRecordingTranscriptTextSelectionAction.join(", ")}],
56
+ "workflow-block": [${surfaceImportNamesBySurfaceType.workflowBlock.join(", ")}]
57
+ });`;
58
+ const importAssetsJS = assets
59
+ .map((asset, index) => `import A${index} from ${JSON.stringify(asset.path)};`)
60
+ .join("\n");
61
+ const registerAssetsJS = `
62
+ const assets = [];
63
+
64
+ ${assets
65
+ .map((asset, index) => `assets.push({name: ${JSON.stringify(asset.name)}, data: A${index}});`)
66
+ .join("\n")}
67
+
68
+ registerAssets(assets);
69
+ `;
70
+ return complete(`
71
+ ${importSurfacesJS}
72
+
73
+ ${importAssetsJS}
74
+
75
+ ${registerSurfacesJS}
76
+
77
+ ${registerAssetsJS}
78
+ `);
79
+ }
@@ -1,5 +1,8 @@
1
1
  import { Command } from "commander";
2
2
  import { isErrored } from "@attio/fetchable-npm";
3
+ import { USE_APP_TS } from "../env.js";
4
+ import { ensureAppEntryPoint } from "../util/ensure-app-entry-point.js";
5
+ import { exitWithMissingEntryPoint } from "../util/exit-with-missing-entry-point.js";
3
6
  import { hardExit } from "../util/hard-exit.js";
4
7
  import { spinnerify } from "../util/spinner.js";
5
8
  import { printJsError, printTsError } from "../util/typescript.js";
@@ -10,6 +13,12 @@ import { printBuildContextError } from "./dev/prepare-build-contexts.js";
10
13
  export const build = new Command("build")
11
14
  .description("Build your Attio extension locally")
12
15
  .action(async () => {
16
+ if (USE_APP_TS) {
17
+ const appEntryPointResult = await ensureAppEntryPoint();
18
+ if (isErrored(appEntryPointResult)) {
19
+ exitWithMissingEntryPoint();
20
+ }
21
+ }
13
22
  const generateGraphqlOperationsResult = await spinnerify("Generating GraphQL types...", "GraphQL types generated successfully", graphqlCodeGen);
14
23
  if (isErrored(generateGraphqlOperationsResult)) {
15
24
  hardExit(generateGraphqlOperationsResult.error.error.toString());
@@ -6,9 +6,11 @@ import tmp from "tmp-promise";
6
6
  import { combineAsync, complete, errored, isErrored } from "@attio/fetchable-npm";
7
7
  import { createClientBuildConfig } from "../../build/client/create-client-build-config.js";
8
8
  import { generateClientEntry } from "../../build/client/generate-client-entry.js";
9
+ import { legacyGenerateClientEntry } from "../../build/client/legacy-generate-client-entry.js";
9
10
  import { createServerBuildConfig } from "../../build/server/create-server-build-config.js";
10
11
  import { generateServerEntry } from "../../build/server/generate-server-entry.js";
11
12
  import { errorsAndWarningsSchema } from "../../build.js";
13
+ import { USE_APP_TS } from "../../env.js";
12
14
  export async function prepareBuildContexts(mode) {
13
15
  const srcDir = "src";
14
16
  const assetsDir = path.join(srcDir, "assets");
@@ -20,10 +22,15 @@ export async function prepareBuildContexts(mode) {
20
22
  .then(async (tempFile) => {
21
23
  let lastJS;
22
24
  const updateTempFile = async () => {
23
- const jsResult = await generateClientEntry({
24
- srcDirAbsolute: path.resolve(srcDir),
25
- assetsDirAbsolute: path.resolve(assetsDir),
26
- });
25
+ const jsResult = USE_APP_TS
26
+ ? await generateClientEntry({
27
+ srcDirAbsolute: path.resolve(srcDir),
28
+ assetsDirAbsolute: path.resolve(assetsDir),
29
+ })
30
+ : await legacyGenerateClientEntry({
31
+ srcDirAbsolute: path.resolve(srcDir),
32
+ assetsDirAbsolute: path.resolve(assetsDir),
33
+ });
27
34
  if (isErrored(jsResult)) {
28
35
  return jsResult;
29
36
  }
@@ -65,7 +72,10 @@ export async function prepareBuildContexts(mode) {
65
72
  }
66
73
  return complete({
67
74
  rebuild: async () => {
68
- await updateTempFile();
75
+ const updateResult = await updateTempFile();
76
+ if (isErrored(updateResult)) {
77
+ return updateResult;
78
+ }
69
79
  try {
70
80
  return complete(await esbuildContext.rebuild());
71
81
  }
@@ -197,6 +207,12 @@ export function printBuildContextError(error) {
197
207
  case "ERROR_FINDING_SURFACE_EXPORTS":
198
208
  process.stderr.write(`${chalk.red("✖ ")}Failed to find surface exports: ${error.error}\n`);
199
209
  break;
210
+ case "APP_ENTRY_POINT_NOT_FOUND":
211
+ process.stderr.write(`${chalk.red("✖ ")}Could not find app.ts\n`);
212
+ break;
213
+ case "APP_EXPORT_NOT_FOUND":
214
+ process.stderr.write(`${chalk.red("✖ ")}Could not find a named export "app" in ${path.basename(error.path)}.\n`);
215
+ break;
200
216
  case "UNPARSABLE_BUILD_ERROR":
201
217
  process.stderr.write(`${chalk.red("✖ ")}Failed to parse build error: ${error.error}\n`);
202
218
  break;
@@ -4,7 +4,10 @@ import notifier from "node-notifier";
4
4
  import { z } from "zod";
5
5
  import { isErrored } from "@attio/fetchable-npm";
6
6
  import { authenticator } from "../auth/auth.js";
7
+ import { USE_APP_TS } from "../env.js";
7
8
  import { printUploadError } from "../print-errors.js";
9
+ import { ensureAppEntryPoint } from "../util/ensure-app-entry-point.js";
10
+ import { hardExit } from "../util/hard-exit.js";
8
11
  import { printMessage } from "../util/print-message.js";
9
12
  import { printJsError, printTsError } from "../util/typescript.js";
10
13
  import { boot } from "./dev/boot.js";
@@ -46,6 +49,17 @@ export const dev = new Command("dev")
46
49
  const { workspace: workspaceSlug } = optionsSchema.parse(unparsedOptions);
47
50
  const cleanupFunctions = [];
48
51
  let isCleaningUp = false;
52
+ if (USE_APP_TS) {
53
+ const appEntryPointResult = await ensureAppEntryPoint(true);
54
+ if (isErrored(appEntryPointResult)) {
55
+ switch (appEntryPointResult.error.code) {
56
+ case "APP_ENTRY_POINT_NOT_FOUND":
57
+ hardExit("Could not find app.ts");
58
+ case "FAILED_TO_GENERATE_ENTRY_POINT":
59
+ hardExit("Failed to generate app.ts");
60
+ }
61
+ }
62
+ }
49
63
  await authenticator.ensureAuthed();
50
64
  const cleanup = async () => {
51
65
  if (isCleaningUp)
@@ -98,18 +112,18 @@ export const dev = new Command("dev")
98
112
  process.stderr.write(error);
99
113
  });
100
114
  cleanupFunctions.push(cleanupGraphqlCodeGen);
101
- let haveJsErrors = false;
115
+ let haveBundlingErrors = false;
102
116
  const cleanupJs = bundleJavaScript(async (contents) => {
103
- if (haveJsErrors) {
104
- process.stdout.write(`${chalk.green("✓")} JavaScript errors fixed\n`);
105
- haveJsErrors = false;
117
+ if (haveBundlingErrors) {
118
+ process.stdout.write(`${chalk.green("✓")} Bundling errors fixed\n`);
119
+ haveBundlingErrors = false;
106
120
  }
107
121
  const uploadResult = await upload({ contents, devVersionId, appId });
108
122
  if (isErrored(uploadResult)) {
109
123
  printUploadError(uploadResult.error);
110
124
  }
111
125
  }, async (error) => {
112
- haveJsErrors = true;
126
+ haveBundlingErrors = true;
113
127
  if (error.code === "BUILD_JAVASCRIPT_ERROR") {
114
128
  notifyJsErrors(error);
115
129
  const { errors, warnings } = error;
@@ -3,10 +3,13 @@ import { Command } from "commander";
3
3
  import { combineAsync, isErrored } from "@attio/fetchable-npm";
4
4
  import { api } from "../../api/api.js";
5
5
  import { authenticator } from "../../auth/auth.js";
6
+ import { USE_APP_TS } from "../../env.js";
6
7
  import { printCliVersionError, printFetcherError, printPackageJsonError } from "../../print-errors.js";
7
8
  import { getAppInfo } from "../../spinners/get-app-info.spinner.js";
8
9
  import { getAppSlugFromPackageJson } from "../../spinners/get-app-slug-from-package-json.js";
9
10
  import { getVersions } from "../../spinners/get-versions.spinner.js";
11
+ import { ensureAppEntryPoint } from "../../util/ensure-app-entry-point.js";
12
+ import { exitWithMissingEntryPoint } from "../../util/exit-with-missing-entry-point.js";
10
13
  import { loadAttioCliVersion } from "../../util/load-attio-cli-version.js";
11
14
  import { spinnerify } from "../../util/spinner.js";
12
15
  import { printJsError } from "../../util/typescript.js";
@@ -16,6 +19,12 @@ import { bundleJavaScript } from "./create/bundle-javascript.js";
16
19
  export const versionCreate = new Command("create")
17
20
  .description("Create a new unpublished version of your Attio app")
18
21
  .action(async () => {
22
+ if (USE_APP_TS) {
23
+ const appEntryPointResult = await ensureAppEntryPoint();
24
+ if (isErrored(appEntryPointResult)) {
25
+ exitWithMissingEntryPoint();
26
+ }
27
+ }
19
28
  await authenticator.ensureAuthed();
20
29
  const appSlugResult = await getAppSlugFromPackageJson();
21
30
  if (isErrored(appSlugResult)) {
package/lib/env.js CHANGED
@@ -3,3 +3,4 @@ const DOMAIN = `attio.${IS_DEV ? "me" : "com"}`;
3
3
  export const API = `https://build.${DOMAIN}/api`;
4
4
  export const APP_NO_PROTOCOL = `app.${DOMAIN}`;
5
5
  export const APP = `https://${APP_NO_PROTOCOL}`;
6
+ export const USE_APP_TS = process.env.USE_APP_TS === "true";
@@ -0,0 +1,30 @@
1
+ import path from "path";
2
+ import readline from "readline/promises";
3
+ import { complete, errored, isErrored } from "@attio/fetchable-npm";
4
+ import { generateAppEntryPoint } from "./generate-app-entry-point.js";
5
+ import { getAppEntryPoint } from "./get-app-entry-point.js";
6
+ export async function ensureAppEntryPoint(promptToGenerate = false) {
7
+ const srcDirAbsolute = path.resolve("src");
8
+ const appEntryPoint = await getAppEntryPoint(srcDirAbsolute);
9
+ if (appEntryPoint !== null) {
10
+ return complete(true);
11
+ }
12
+ if (!promptToGenerate) {
13
+ return errored({
14
+ code: "APP_ENTRY_POINT_NOT_FOUND",
15
+ });
16
+ }
17
+ let readlineInterface = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout,
20
+ });
21
+ await readlineInterface.question("Could not find app.ts entry point. Press Enter to generate it...");
22
+ readlineInterface.close();
23
+ const generateResult = await generateAppEntryPoint(srcDirAbsolute);
24
+ if (isErrored(generateResult)) {
25
+ return errored({
26
+ code: "FAILED_TO_GENERATE_ENTRY_POINT",
27
+ });
28
+ }
29
+ return complete(true);
30
+ }
@@ -0,0 +1,11 @@
1
+ import { hardExit } from "./hard-exit.js";
2
+ export function exitWithMissingEntryPoint() {
3
+ const packageManager = process.env.npm_config_user_agent?.split("/")?.[0];
4
+ const command = {
5
+ npm: "npm run dev",
6
+ yarn: "yarn dev",
7
+ pnpm: "pnpm dev",
8
+ bun: "bun run dev",
9
+ }[packageManager ?? "npm"] ?? "npm run dev";
10
+ hardExit(`Could not find app.ts. Run \`${command}\` to generate it`);
11
+ }
@@ -14,8 +14,8 @@ export function parseFileExports({ sourceFile, existingExportSymbols, existingId
14
14
  return aliasedSymbol ?? declarationSymbol;
15
15
  }
16
16
  const declarationsByName = sourceFile.getExportedDeclarations();
17
- for (const surfaceName of SURFACE_TYPES) {
18
- const declarations = declarationsByName.get(surfaceName);
17
+ for (const surfaceType of SURFACE_TYPES) {
18
+ const declarations = declarationsByName.get(surfaceType);
19
19
  if (!declarations) {
20
20
  continue;
21
21
  }
@@ -27,12 +27,12 @@ export function parseFileExports({ sourceFile, existingExportSymbols, existingId
27
27
  }
28
28
  const exportType = declaration.getType();
29
29
  if (isTypeScript(sourceFile) &&
30
- !typeChecker.isTypeAssignableTo(exportType, surfaceTypes[surfaceName])) {
30
+ !typeChecker.isTypeAssignableTo(exportType, surfaceTypes[surfaceType])) {
31
31
  const node = declaration.getFirstChild() || declaration;
32
32
  throw {
33
33
  errors: [
34
34
  {
35
- text: `${surfaceName} in ${filePath} is not assignable to ${getSurfaceTypeName(surfaceName)}`,
35
+ text: `${surfaceType} in ${filePath} is not assignable to ${getSurfaceTypeName(surfaceType)}`,
36
36
  location: {
37
37
  file: filePath,
38
38
  line: node.getStartLineNumber(),
@@ -40,8 +40,8 @@ export function parseFileExports({ sourceFile, existingExportSymbols, existingId
40
40
  lineText: declaration.getText().split("\n")[0],
41
41
  additionalLines: declaration.getText().split("\n").slice(1),
42
42
  length: node.getWidth(),
43
- namespace: surfaceName,
44
- suggestion: `Ensure the export matches the type ${getSurfaceTypeName(surfaceName)}`,
43
+ namespace: surfaceType,
44
+ suggestion: `Ensure the export matches the type ${getSurfaceTypeName(surfaceType)}`,
45
45
  },
46
46
  },
47
47
  ],
@@ -49,24 +49,24 @@ export function parseFileExports({ sourceFile, existingExportSymbols, existingId
49
49
  }
50
50
  function getPropertyValueOrThrow(propertyName) {
51
51
  if (declaration.getKind() !== SyntaxKind.VariableDeclaration) {
52
- throw new Error(`${surfaceName} is not an object in ${filePath} ${declaration.getKind()}`);
52
+ throw new Error(`${surfaceType} is not an object in ${filePath} ${declaration.getKind()}`);
53
53
  }
54
54
  const initializer = declaration
55
55
  .asKindOrThrow(SyntaxKind.VariableDeclaration)
56
56
  .getInitializer();
57
57
  if (initializer?.getKind() !== SyntaxKind.ObjectLiteralExpression) {
58
- throw new Error(`${surfaceName} must be defined as an object literal expression in ${filePath}`);
58
+ throw new Error(`${surfaceType} must be defined as an object literal expression in ${filePath}`);
59
59
  }
60
60
  const objectLiteral = initializer.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
61
61
  const property = objectLiteral.getProperty(propertyName);
62
62
  if (property?.getKind() !== SyntaxKind.PropertyAssignment) {
63
- throw new Error(`Property ${propertyName} on ${surfaceName} must be directly declared in the object declaration in ${filePath}`);
63
+ throw new Error(`Property ${propertyName} on ${surfaceType} must be directly declared in the object declaration in ${filePath}`);
64
64
  }
65
65
  const propertyInitializer = property
66
66
  .asKindOrThrow(SyntaxKind.PropertyAssignment)
67
67
  .getInitializer();
68
68
  if (propertyInitializer?.getKind() !== SyntaxKind.StringLiteral) {
69
- throw new Error(`Property ${propertyName} on ${surfaceName} must be a string literal in ${filePath}`);
69
+ throw new Error(`Property ${propertyName} on ${surfaceType} must be a string literal in ${filePath}`);
70
70
  }
71
71
  return propertyInitializer.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
72
72
  }
@@ -83,7 +83,7 @@ export function parseFileExports({ sourceFile, existingExportSymbols, existingId
83
83
  length: declaration.getWidth(),
84
84
  lineText: declaration.getText().split("\n")[0],
85
85
  additionalLines: declaration.getText().split("\n").slice(1),
86
- namespace: surfaceName,
86
+ namespace: surfaceType,
87
87
  suggestion: `Ensure the id is unique`,
88
88
  },
89
89
  },
@@ -92,7 +92,7 @@ export function parseFileExports({ sourceFile, existingExportSymbols, existingId
92
92
  }
93
93
  existingExportSymbols.add(originalSymbol);
94
94
  existingIds.add(surfaceId);
95
- surfaceExports.add(surfaceName);
95
+ surfaceExports.add({ surfaceType, id: surfaceId });
96
96
  }
97
97
  return surfaceExports;
98
98
  }
@@ -0,0 +1,75 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { complete, fromPromise, isErrored } from "@attio/fetchable-npm";
4
+ import { findSurfaceExports } from "./find-surface-exports/find-surface-exports.js";
5
+ import { SURFACE_TYPES } from "./surfaces.js";
6
+ import { toCamelCase } from "./to-camel-case.js";
7
+ export async function generateAppEntryPoint(srcDirAbsolute) {
8
+ const surfaceExports = await findSurfaceExports(srcDirAbsolute);
9
+ const surfaceImportFilesByType = SURFACE_TYPES.reduce((surfaceExports, surfaceType) => {
10
+ surfaceExports[surfaceType] = [];
11
+ return surfaceExports;
12
+ }, {});
13
+ for (const [filePath, surfaces] of surfaceExports) {
14
+ for (const surface of surfaces) {
15
+ surfaceImportFilesByType[surface.surfaceType].push({
16
+ importPath: getRelativeImportPath(srcDirAbsolute, filePath),
17
+ id: surface.id,
18
+ });
19
+ }
20
+ }
21
+ const appImportStatement = `import type {App} from "attio/client"`;
22
+ const surfaceImportsStatements = SURFACE_TYPES.flatMap((surfaceType) => {
23
+ const surfaceImports = surfaceImportFilesByType[surfaceType];
24
+ return surfaceImports.map((surface) => {
25
+ const surfaceName = getSurfaceImportName(surface.id);
26
+ return `import {${surfaceType}${surfaceName !== surfaceType ? ` as ${surfaceName}` : ""}} from ${JSON.stringify(surface.importPath)}`;
27
+ });
28
+ }).join("\n");
29
+ const getSurfaceNamesArray = (surfaceType) => {
30
+ const surfaceImports = surfaceImportFilesByType[surfaceType];
31
+ return surfaceImports.map((surface) => getSurfaceImportName(surface.id));
32
+ };
33
+ const appExportStatement = `export const app: App = {
34
+ record: {
35
+ actions: [${getSurfaceNamesArray("recordAction").join(",")}],
36
+ bulkActions: [${getSurfaceNamesArray("bulkRecordAction").join(",")}],
37
+ widgets: [${getSurfaceNamesArray("recordWidget").join(",")}],
38
+ },
39
+ callRecording: {
40
+ insight: {
41
+ textActions: [${getSurfaceNamesArray("callRecordingInsightTextSelectionAction")}]
42
+ },
43
+ summary: {
44
+ textActions: [${getSurfaceNamesArray("callRecordingSummaryTextSelectionAction")}]
45
+ },
46
+ transcript: {
47
+ textActions: [${getSurfaceNamesArray("callRecordingTranscriptTextSelectionAction")}]
48
+ },
49
+ },
50
+ }`;
51
+ const appEntryPointContent = [
52
+ appImportStatement,
53
+ surfaceImportsStatements,
54
+ appExportStatement,
55
+ ].join("\n\n");
56
+ const writeResult = await fromPromise(fs.writeFile(path.join(srcDirAbsolute, "app.ts"), appEntryPointContent));
57
+ if (isErrored(writeResult)) {
58
+ return writeResult;
59
+ }
60
+ return complete(true);
61
+ }
62
+ function getRelativeImportPath(dir, filePath) {
63
+ const relativePath = path
64
+ .relative(dir, filePath)
65
+ .split(path.sep)
66
+ .join("/")
67
+ .replace(/\.[^/.]+$/, "");
68
+ if (relativePath.startsWith(".")) {
69
+ return relativePath;
70
+ }
71
+ return `./${relativePath}`;
72
+ }
73
+ function getSurfaceImportName(surfaceId) {
74
+ return toCamelCase(surfaceId);
75
+ }
@@ -0,0 +1,18 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { fromPromise, isErrored } from "@attio/fetchable-npm";
4
+ const APP_ENTRY_POINT_EXTENSIONS = ["ts", "tsx"];
5
+ export async function getAppEntryPoint(srcDirAbsolute) {
6
+ const appEntryPoints = await Promise.all(APP_ENTRY_POINT_EXTENSIONS.map(async (extension) => {
7
+ const filePath = path.join(srcDirAbsolute, `app.${extension}`);
8
+ const contentResult = await fromPromise(fs.readFile(filePath));
9
+ if (isErrored(contentResult)) {
10
+ return null;
11
+ }
12
+ return {
13
+ path: filePath,
14
+ content: contentResult.value.toString(),
15
+ };
16
+ }));
17
+ return appEntryPoints.find((entryPoint) => entryPoint !== null) ?? null;
18
+ }
@@ -0,0 +1,13 @@
1
+ export function toCamelCase(input) {
2
+ return input
3
+ .replace(/[^a-zA-Z0-9]+/g, " ")
4
+ .trim()
5
+ .split(" ")
6
+ .map((word, index) => {
7
+ if (index === 0) {
8
+ return word.toLowerCase();
9
+ }
10
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
11
+ })
12
+ .join("");
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attio",
3
- "version": "0.0.1-experimental.20250825",
3
+ "version": "0.0.1-experimental.20250829.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "lib",
package/schema.graphql CHANGED
@@ -11,6 +11,12 @@ interface IObject {
11
11
  The slug of the object.
12
12
  """
13
13
  slug: String!
14
+ attributes(
15
+ """
16
+ Filter attributes by types.
17
+ """
18
+ types: [AttributeType!]
19
+ ): [Attribute!]!
14
20
  record(
15
21
  """
16
22
  ID of the record.
@@ -25,6 +31,38 @@ interface IObject {
25
31
  ): [Record]!
26
32
  }
27
33
 
34
+ """
35
+ An attribute of an object.
36
+ """
37
+ type Attribute {
38
+ title: String!
39
+ slug: String!
40
+ type: AttributeType!
41
+ }
42
+
43
+ """
44
+ The type of an attribute.
45
+ """
46
+ enum AttributeType {
47
+ TEXT
48
+ NUMBER
49
+ CHECKBOX
50
+ CURRENCY
51
+ DATE
52
+ TIMESTAMP
53
+ RATING
54
+ PIPELINE
55
+ SELECT
56
+ ENTITY_REFERENCE
57
+ ACTOR_REFERENCE
58
+ LOCATION
59
+ DOMAIN
60
+ EMAIL_ADDRESS
61
+ PHONE_NUMBER
62
+ INTERACTION
63
+ PERSONAL_NAME
64
+ }
65
+
28
66
  """
29
67
  A record in the workspace.
30
68
  """
@@ -256,6 +294,12 @@ type Object implements IObject {
256
294
  The slug of the object.
257
295
  """
258
296
  slug: String!
297
+ attributes(
298
+ """
299
+ Filter attributes by types.
300
+ """
301
+ types: [AttributeType!]
302
+ ): [Attribute!]!
259
303
  record(
260
304
  """
261
305
  ID of the record.
@@ -422,6 +466,12 @@ type people implements IObject {
422
466
  The slug of the object.
423
467
  """
424
468
  slug: String!
469
+ attributes(
470
+ """
471
+ Filter attributes by types.
472
+ """
473
+ types: [AttributeType!]
474
+ ): [Attribute!]!
425
475
  record(
426
476
  """
427
477
  ID of the person record.
@@ -657,6 +707,12 @@ type companies implements IObject {
657
707
  The slug of the object.
658
708
  """
659
709
  slug: String!
710
+ attributes(
711
+ """
712
+ Filter attributes by types.
713
+ """
714
+ types: [AttributeType!]
715
+ ): [Attribute!]!
660
716
  record(
661
717
  """
662
718
  ID of the company record.
@@ -684,6 +740,12 @@ type deals implements IObject {
684
740
  The slug of the object.
685
741
  """
686
742
  slug: String!
743
+ attributes(
744
+ """
745
+ Filter attributes by types.
746
+ """
747
+ types: [AttributeType!]
748
+ ): [Attribute!]!
687
749
  record(
688
750
  """
689
751
  ID of the deal record.
@@ -711,6 +773,12 @@ type workspaces implements IObject {
711
773
  The slug of the object.
712
774
  """
713
775
  slug: String!
776
+ attributes(
777
+ """
778
+ Filter attributes by types.
779
+ """
780
+ types: [AttributeType!]
781
+ ): [Attribute!]!
714
782
  record(
715
783
  """
716
784
  ID of the workspace record.
@@ -738,6 +806,12 @@ type users implements IObject {
738
806
  The slug of the object.
739
807
  """
740
808
  slug: String!
809
+ attributes(
810
+ """
811
+ Filter attributes by types.
812
+ """
813
+ types: [AttributeType!]
814
+ ): [Attribute!]!
741
815
  record(
742
816
  """
743
817
  ID of the user record.