attio 0.0.1-experimental.20250829 → 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.20250829",
3
+ "version": "0.0.1-experimental.20250829.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "lib",