Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/manager/src/constants/API_ENDPOINTS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type APIEndpoints = {
RepositoryService: string;
LocaleService: string;
CustomTypeService: string;
GitService: string;
};

export const API_ENDPOINTS: APIEndpoints = (() => {
Expand Down Expand Up @@ -47,6 +48,9 @@ export const API_ENDPOINTS: APIEndpoints = (() => {
process.env.custom_type_api ??
"https://api.internal.wroom.io/custom-type/",
),
GitService: addTrailingSlash(
process.env.git_service_api ?? "https://api.internal.wroom.io/git/",
),
};

const missingAPIEndpoints = Object.keys(apiEndpoints).filter((key) => {
Expand Down Expand Up @@ -96,6 +100,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
RepositoryService: "https://api.internal.wroom.io/repository/",
LocaleService: "https://api.internal.wroom.io/locale/",
CustomTypeService: "https://api.internal.wroom.io/custom-type/",
GitService: "https://api.internal.wroom.io/git/",
};
}

Expand All @@ -114,6 +119,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
RepositoryService: `https://api.internal.${process.env.SM_ENV}-wroom.com/repository/`,
LocaleService: `https://api.internal.${process.env.SM_ENV}-wroom.com/locale/`,
CustomTypeService: `https://api.internal.${process.env.SM_ENV}-wroom.com/custom-type/`,
GitService: `https://api.internal.${process.env.SM_ENV}-wroom.com/git/`,
};
}

Expand All @@ -131,6 +137,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
RepositoryService: "https://api.internal.prismic.io/repository/",
LocaleService: "https://api.internal.prismic.io/locale/",
CustomTypeService: "https://api.internal.prismic.io/custom-type/",
GitService: "https://api.internal.prismic.io/git/",
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,34 @@ type PrismicRepositoryManagerFetchEnvironmentsReturnType = {
environments?: Environment[];
};

const GitIntegrationsSchema = z.object({
integrations: z.array(
z.discriminatedUnion("status", [
z.object({
id: z.string(),
status: z.literal("connected"),
owner: z.string(),
repositories: z.array(
z.object({
name: z.string(),
fullName: z.string(),
}),
),
}),
z.object({
id: z.string(),
status: z.literal("broken"),
owner: z.string().optional(),
repositories: z.tuple([]),
}),
]),
),
});

const GitIntegrationTokenSchema = z.object({
token: z.string(),
});

export class PrismicRepositoryManager extends BaseManager {
// TODO: Add methods for repository-specific actions. E.g. creating a
// new repository.
Expand Down Expand Up @@ -705,6 +733,68 @@ export class PrismicRepositoryManager extends BaseManager {
}
}

async fetchGitIntegrations(): Promise<z.infer<typeof GitIntegrationsSchema>> {
const repositoryName = await this.project.getRepositoryName();

const url = new URL("integrations", API_ENDPOINTS.GitService);
url.searchParams.set("repository", repositoryName);

const res = await this._fetch({ url });

if (!res.ok) {
const text = await res.text();
throw new Error(
`Failed to fetch integrations for repository ${repositoryName}`,
{ cause: text },
);
}

const json = await res.json();
const { value, error } = decode(GitIntegrationsSchema, json);

if (error) {
throw new UnexpectedDataError(
`Failed to decode integrations: ${error.errors.join(", ")}`,
);
}

return value;
}

async fetchGitIntegrationToken(args: {
integrationId: string;
}): Promise<z.infer<typeof GitIntegrationTokenSchema>> {
const { integrationId } = args;
const repositoryName = await this.project.getRepositoryName();

const url = new URL(
`integrations/${integrationId}/token`,
API_ENDPOINTS.GitService,
);
url.searchParams.set("repository", repositoryName);

const res = await this._fetch({ url, method: "POST" });

if (!res.ok) {
const text = await res.text();
throw new Error(
`Failed to fetch token for integration ${integrationId}`,
{ cause: text },
);
}

const json = await res.json();
const { value, error } = decode(GitIntegrationTokenSchema, json);

if (error) {
throw new UnexpectedDataError(
`Failed to decode integration token: ${error.errors.join(", ")}`,
);
}

return value;
}

private _decodeLimitOrThrow(
potentialLimit: unknown,
statusCode: number,
Expand Down
92 changes: 92 additions & 0 deletions packages/manager/src/managers/slices/SlicesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
SliceRenameHookData,
SliceUpdateHook,
} from "@slicemachine/plugin-kit";
import { writeSliceFile } from "@slicemachine/plugin-kit/fs";
import pLimit from "p-limit";

import { DecodeError } from "../../lib/DecodeError";
import { assertPluginsInitialized } from "../../lib/assertPluginsInitialized";
Expand Down Expand Up @@ -98,6 +100,22 @@ type SliceMachineManagerReadSliceScreenshotArgs = {
variationID: string;
};

type SliceFile = {
path: string;
contents: string | Buffer; // String for plain text files, Buffer for binary files
isBinary: boolean;
};

type SliceMachineManagerWriteSliceFilesArgs = {
libraryID: string;
sliceID: string;
files: SliceFile[];
};

type SliceMachineManagerWriteSliceFilesReturnType = {
errors: (HookError | DecodeError)[];
};

type SliceMachineManagerReadSliceScreenshotReturnType = {
data: Buffer | undefined;
errors: (DecodeError | HookError)[];
Expand Down Expand Up @@ -1126,4 +1144,78 @@ export class SlicesManager extends BaseManager {

return { errors: customTypeReadErrors };
}

async writeSliceFiles(
args: SliceMachineManagerWriteSliceFilesArgs,
): Promise<SliceMachineManagerWriteSliceFilesReturnType> {
assertPluginsInitialized(this.sliceMachinePluginRunner);

const { libraryID, sliceID, files } = args;

// Read the slice model to get helpers
const { model, errors: readSliceErrors } = await this.readSlice({
libraryID,
sliceID,
});

if (!model) {
return {
errors: readSliceErrors,
};
}

// Write each file using writeSliceFile from plugin-kit with bounded concurrency
const errors: HookError[] = [];
const helpers = this.sliceMachinePluginRunner.rawHelpers;
const limit = pLimit(8);
await Promise.all(
files.map((file) =>
limit(async () => {
try {
const writtenPath = await writeSliceFile({
libraryID,
model,
filename: file.path,
contents: file.contents,
helpers,
});
console.info(
`Successfully wrote file: ${file.path} -> ${writtenPath}`,
);
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: `Failed to write file ${file.path}`;

const errorStack = error instanceof Error ? error.stack : undefined;
console.error(`Error writing file ${file.path}:`, errorMessage);
if (errorStack) {
console.error(`Stack trace:`, errorStack);
}

const owner =
this.sliceMachinePluginRunner?.hooksForType(
"slice:asset:update",
)[0]?.meta.owner ?? "SlicesManager";

errors.push(
new HookError(
{
id: "writeSliceFile",
type: "slice:asset:update",
owner,
},
error,
),
);
}
}),
),
);

return {
errors,
};
}
}
89 changes: 88 additions & 1 deletion packages/manager/src/managers/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ export const SegmentEventType = {
mcp_promo_link_clicked: "mcp:promo-link-clicked",
info_banner_dismissed: "info-banner:dismissed",
info_banner_button_clicked: "info-banner:button-clicked",
slice_library_opened: "slice-library:opened",
slice_library_projects_listed: "slice-library:projects-listed",
slice_library_slice_selected: "slice-library:slice-selected",
slice_library_fetching_started: "slice-library:fetching-started",
slice_library_fetching_ended: "slice-library:fetching-ended",
slice_library_import_started: "slice-library:import-started",
slice_library_import_ended: "slice-library:import-ended",
} as const;
type SegmentEventTypes =
(typeof SegmentEventType)[keyof typeof SegmentEventType];
Expand Down Expand Up @@ -127,6 +134,20 @@ export const HumanSegmentEventType = {
"SliceMachine Info Banner Dismissed",
[SegmentEventType.info_banner_button_clicked]:
"SliceMachine Info Banner Button Clicked",
[SegmentEventType.slice_library_opened]:
"SliceMachine Slice Library - Opened",
[SegmentEventType.slice_library_projects_listed]:
"SliceMachine Slice Library - Projects Listed",
[SegmentEventType.slice_library_slice_selected]:
"SliceMachine Slice Library - Slice Selected",
[SegmentEventType.slice_library_fetching_started]:
"SliceMachine Slice Library - Slice Fetching Started",
[SegmentEventType.slice_library_fetching_ended]:
"SliceMachine Slice Library - Slice Fetching Ended",
[SegmentEventType.slice_library_import_started]:
"SliceMachine Slice Library - Slice Import Started",
[SegmentEventType.slice_library_import_ended]:
"SliceMachine Slice Library - Slice Import Ended",
} as const;

export type HumanSegmentEventTypes =
Expand Down Expand Up @@ -301,6 +322,7 @@ type SliceCreatedSegmentEvent = SegmentEvent<
| { mode: "figma-to-slice" }
| { mode: "manual" }
| { mode: "template"; sliceTemplate: string }
| { mode: "import" }
)
>;

Expand Down Expand Up @@ -505,6 +527,64 @@ type InfoBannerButtonClicked = SegmentEvent<
}
>;

type SliceLibraryOpened = SegmentEvent<
typeof SegmentEventType.slice_library_opened
>;
type SliceLibraryProjectsListed = SegmentEvent<
typeof SegmentEventType.slice_library_projects_listed,
{
repositories_count: number;
}
>;
type SliceLibrarySliceSelected = SegmentEvent<
typeof SegmentEventType.slice_library_slice_selected,
{
slices_count: number;
source_project_id: string;
destination_project_id: string;
}
>;
type SliceLibraryFetchingStarted = SegmentEvent<
typeof SegmentEventType.slice_library_fetching_started,
{
source_project_id: string;
}
>;
type SliceLibraryFetchingEnded = SegmentEvent<
typeof SegmentEventType.slice_library_fetching_ended,
| {
error: false;
slices_count: number;
source_project_id: string;
}
| {
error: true;
slices_count?: never;
source_project_id: string;
}
>;
type SliceLibraryImportStarted = SegmentEvent<
typeof SegmentEventType.slice_library_import_started,
{
source_project_id: string;
}
>;
type SliceLibraryImportEnded = SegmentEvent<
typeof SegmentEventType.slice_library_import_ended,
| {
error: false;
slices_count: number;
source_project_id: string;
destination_project_id: string;
}
| {
error: true;
slices_count?: never;
source_project_id: string;
destination_project_id: string;
}
>;

export type SegmentEvents =
| CommandInitStartSegmentEvent
| CommandInitIdentifySegmentEvent
Expand Down Expand Up @@ -550,4 +630,11 @@ export type SegmentEvents =
| SidebarLinkClicked
| McpPromoLinkClicked
| InfoBannerDismissed
| InfoBannerButtonClicked;
| InfoBannerButtonClicked
| SliceLibraryOpened
| SliceLibraryProjectsListed
| SliceLibrarySliceSelected
| SliceLibraryFetchingStarted
| SliceLibraryFetchingEnded
| SliceLibraryImportStarted
| SliceLibraryImportEnded;
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ it("returns global Slice Machine state", async () => {
RepositoryService: "https://api.internal.prismic.io/repository/",
LocaleService: "https://api.internal.prismic.io/locale/",
CustomTypeService: "https://api.internal.prismic.io/custom-type/",
GitService: "https://api.internal.prismic.io/git/",
});
expect(result.clientError).toStrictEqual({
name: new UnauthenticatedError().name,
Expand Down
6 changes: 3 additions & 3 deletions packages/slice-machine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
"@emotion/react": "11.11.1",
"@extractus/oembed-extractor": "3.1.8",
"@prismicio/client": "7.17.0",
"@prismicio/editor-fields": "0.4.88",
"@prismicio/editor-support": "0.4.88",
"@prismicio/editor-ui": "0.4.88",
"@prismicio/editor-fields": "0.4.90",
"@prismicio/editor-support": "0.4.90",
"@prismicio/editor-ui": "0.4.90",
"@prismicio/mock": "0.7.1",
"@prismicio/mocks": "2.14.0",
"@prismicio/simulator": "0.1.4",
Expand Down
Loading
Loading