@slicemachine/init 1.1.10-alpha.5 → 1.1.11-alpha.2

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,11 +1,13 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
- import axios, { AxiosError } from "axios";
3
+ import type { AxiosError } from "axios";
4
+ import { Models } from "@slicemachine/core";
4
5
 
5
6
  import * as t from "io-ts";
6
7
  import { getOrElseW } from "fp-ts/Either";
7
- import { handleErrors } from "./communication";
8
- import { logs } from "../../utils";
8
+ import { InitClient, logs, lsdir, lsfiles } from "../../utils";
9
+ import { PrismicSharedConfigManager } from "@slicemachine/core/build/prismic/SharedConfig";
10
+ import Tracker from "../../utils/tracker";
9
11
 
10
12
  const SignatureFileReader = t.type({
11
13
  signature: t.string,
@@ -23,92 +25,71 @@ export async function readSignatureFile(cwd: string): Promise<SignatureFile> {
23
25
  });
24
26
  }
25
27
 
26
- async function lsdir(dir: string): Promise<Array<string>> {
27
- return fs.promises.readdir(dir).then((dirs) => {
28
- return dirs
29
- .filter((name) => fs.statSync(path.join(dir, name)).isDirectory())
30
- .map((subdirectory) => path.join(dir, subdirectory));
31
- });
32
- }
33
-
34
- async function lsfiles(dir: string): Promise<Array<string>> {
35
- return fs.promises.readdir(dir).then((dirs) => {
36
- return dirs
37
- .filter((name) => fs.statSync(path.join(dir, name)).isFile())
38
- .map((file) => path.join(dir, file));
39
- });
40
- }
41
-
42
- export async function readDocuments(cwd: string) {
28
+ export async function readDocuments(
29
+ cwd: string
30
+ ): Promise<Record<string, unknown>> {
43
31
  const documentDir = path.join(cwd, "documents");
44
32
  const dirs = await lsdir(documentDir);
45
33
 
46
34
  const files = (await Promise.all(dirs.map((dir) => lsfiles(dir)))).flat();
47
35
 
48
36
  const documentObj = files.reduce<Record<string, unknown>>((acc, file) => {
49
- const fileContent = fs.readFileSync(file, "utf-8");
50
- const filename = path.parse(file).name;
51
- acc[filename] = JSON.parse(fileContent);
52
- return acc;
37
+ const filename: string = path.parse(file).name;
38
+ const fileContent: string = fs.readFileSync(file, "utf-8");
39
+ const json = JSON.parse(fileContent) as Record<string, unknown>;
40
+
41
+ return { ...acc, [filename]: json };
53
42
  }, {});
54
43
 
55
- return JSON.stringify(documentObj);
44
+ return documentObj;
56
45
  }
57
46
 
58
- export const sendDocumentsFromStarter = async (
59
- repository: string,
60
- cookies: string,
61
- base: string,
47
+ export async function sendDocuments(
48
+ client: InitClient,
62
49
  cwd: string
63
- ): Promise<boolean> => {
50
+ ): Promise<boolean> {
64
51
  const pathToDocuments = path.join(cwd, "documents");
65
52
  const pathToSignatureFile = path.join(pathToDocuments, "index.json");
66
53
 
67
- if (!fs.existsSync(pathToSignatureFile)) {
68
- return Promise.resolve(false);
69
- }
54
+ // Without the signature, we can't push documents to Prismic.
55
+ if (!fs.existsSync(pathToSignatureFile)) return Promise.resolve(false);
70
56
 
71
57
  const signatureObj = await readSignatureFile(cwd);
72
- const documentsStr = await readDocuments(cwd);
73
-
74
- const payload = {
75
- signature: signatureObj.signature,
76
- documents: documentsStr,
77
- };
58
+ const documents: Record<string, unknown> = await readDocuments(cwd);
78
59
 
79
- const prismicUrl = new URL(base);
80
- prismicUrl.hostname = `${repository}.${prismicUrl.hostname}`;
81
- prismicUrl.pathname = "starter/documents";
82
- const endpointURL = prismicUrl.toString();
60
+ // No documents to push
61
+ if (Object.keys(documents).length === 0) return Promise.resolve(false);
83
62
 
84
63
  const spinner = logs.spinner("Pushing existing documents to your repository");
85
64
  spinner.start();
86
65
 
87
- return axios
88
- .post(endpointURL, payload, {
89
- headers: {
90
- "User-Agent": "prismic-cli/0",
91
- Cookie: cookies,
92
- },
93
- })
66
+ return client
67
+ .pushDocuments(
68
+ signatureObj.signature,
69
+ documents,
70
+ PrismicSharedConfigManager.get().cookies
71
+ )
94
72
  .then(() => {
95
73
  spinner.succeed();
96
74
  fs.rmSync(pathToDocuments, { recursive: true, force: true });
97
75
  return true;
98
76
  })
99
- .catch((e: AxiosError) => {
77
+ .catch(async (e: AxiosError) => {
100
78
  spinner.fail();
101
79
  if (e.response?.data === "Repository should not contain documents") {
102
80
  logs.writeError(
103
81
  "The selected repository is not empty, documents cannot be uploaded. Please choose an empty repository or delete the documents contained in your repository."
104
82
  );
105
83
  } else {
106
- handleErrors(
107
- "sending documents, please try again. If the problem persists, contact us.",
108
- e
84
+ logs.writeError(
85
+ "Sending documents failed, please try again. If the problem persists, contact us."
109
86
  );
87
+ logs.writeError(`Full error: ${e.code || 500} - ${e.message}`);
110
88
  }
111
-
89
+ await Tracker.get().trackInitEndFail(
90
+ Models.Frameworks.none,
91
+ "Failed to push documents"
92
+ );
112
93
  process.exit(1);
113
94
  });
114
- };
95
+ }
@@ -22,7 +22,7 @@ export async function promptToPushCustomTypes(): Promise<boolean> {
22
22
  name: "pushCustomTypes",
23
23
  default: false,
24
24
  message:
25
- "Your repository already contains Custom Types. Do you want to continue pushing your local Slices?",
25
+ "Your repository already contains Custom Types. Do you want to continue pushing your local Custom Types?",
26
26
  },
27
27
  ])
28
28
  .then((res) => res.pushCustomTypes);
@@ -1,201 +1,70 @@
1
- import fs from "fs";
2
- import axios from "axios";
3
- import mime from "mime";
4
- import snakeCase from "lodash.snakecase";
5
- import path from "path";
6
- import FormData from "form-data";
7
- import uniqid from "uniqid";
8
- import { logs } from "../../utils";
1
+ import { InitClient } from "../../utils";
9
2
  import {
10
3
  Component,
11
4
  ComponentInfo,
12
5
  VariationSM,
13
6
  SliceSM,
14
7
  } from "@slicemachine/core/build/models";
8
+ import { Acl, ClientError } from "@slicemachine/client";
9
+ import { writeError } from "../../utils/logs";
15
10
 
16
- export type ALC = {
17
- values: {
18
- url: string;
19
- fields: Record<string, string>;
20
- };
21
- imgixEndpoint: string;
22
- err: null | string;
23
- };
24
-
25
- export async function createAcl(
26
- address: string,
27
- repository: string,
28
- authorization: string
29
- ): Promise<ALC> {
30
- return axios
31
- .get<ALC>(address + "create", {
32
- headers: {
33
- repository,
34
- Authorization: `Bearer ${authorization}`,
35
- "User-Agent": "slice-machine",
36
- },
37
- })
38
- .then((res) => res.data);
39
- }
40
-
41
- async function createFormForS3(
42
- key: string,
43
- filename: string,
44
- filePath: string,
45
- acl: ALC
46
- ): Promise<FormData | null> {
47
- const form = new FormData();
48
- Object.entries(acl.values.fields).forEach(([k, value]) => {
49
- form.append(k, value);
50
- });
51
- form.append("key", key);
52
- const contentType = mime.getType(filePath);
53
- contentType && form.append("Content-Type", contentType);
54
-
55
- return fs.promises
56
- .readFile(filePath)
57
- .then((file) => {
58
- form.append("file", file, { filename });
59
- return form;
60
- })
61
- .catch(() => {
62
- logs.writeError(`Error reading preview image: ${filename}`);
63
- return null;
64
- });
65
- }
66
-
67
- function createS3Key(
68
- repository: string,
69
- sliceName: string,
70
- variationId: string,
71
- filename: string
72
- ): string {
73
- return `${repository}/shared-slices/${snakeCase(sliceName)}/${snakeCase(
74
- variationId
75
- )}-${uniqid()}/${filename}`;
76
- }
77
-
78
- async function sendVariationPreviewToS3(
79
- acl: ALC,
80
- repository: string,
81
- sliceName: string,
82
- variationId: string,
83
- filePath: string
84
- ): Promise<string | null> {
85
- const filename = path.basename(filePath);
86
- const key = createS3Key(repository, sliceName, variationId, filename);
87
- const form = await createFormForS3(key, filename, filePath, acl);
88
- if (form === null) return null;
89
- if (form.hasKnownLength() === false) {
90
- logs.writeError(
91
- `[slice/push] An error occurred while uploading preview image ${filePath} as length in unknown`
92
- );
93
- }
94
-
95
- const errorMessage = `[slice/push] An error occurred while uploading preview images for ${sliceName}-${variationId} - please contact support`;
11
+ async function updateVariationWithScreenshot(
12
+ client: InitClient,
13
+ acl: Acl,
14
+ screenshotPaths: ComponentInfo["screenshotPaths"],
15
+ sliceName: SliceSM["id"],
16
+ variation: VariationSM
17
+ ): Promise<VariationSM> {
18
+ const screenshot = screenshotPaths[variation.id];
19
+ if (!screenshot || !screenshot.path) return Promise.resolve(variation);
96
20
 
97
- return axios
98
- .post(acl.values.url, form, {
99
- headers: {
100
- ...form.getHeaders(),
101
- "Content-Length": String(form.getLengthSync()),
102
- },
21
+ return client
22
+ .uploadScreenshot({
23
+ acl,
24
+ sliceName,
25
+ variationId: variation.id,
26
+ filePath: screenshot.path,
103
27
  })
104
- .then((res) => {
105
- if (res.status !== 204) {
106
- logs.writeError(errorMessage);
107
- logs.writeError(`${res.status}: ${res.statusText}`);
108
- return null;
109
- } else {
110
- return `${acl.imgixEndpoint}/${key}`;
111
- }
28
+ .then((screenshotUrl) => {
29
+ return {
30
+ ...variation,
31
+ imageUrl: screenshotUrl,
32
+ };
112
33
  })
113
- .catch((err) => {
114
- logs.writeError(errorMessage);
115
- if (axios.isAxiosError(err) && err.response) {
116
- logs.writeError(`${err.response.status}: ${err.response.statusText}`);
117
- } else if (err instanceof Error) {
118
- logs.writeError(err.message);
119
- } else {
120
- logs.writeError(String(err));
121
- }
122
- return null;
34
+ .catch((error: ClientError) => {
35
+ writeError(
36
+ `Couldn't upload screenshot slice: ${sliceName} - variation: ${variation.id}`
37
+ );
38
+ writeError(error.message, "Full error:");
39
+ return variation;
123
40
  });
124
41
  }
125
42
 
126
- async function maybeAddImageUrlToVariation(
127
- acl: ALC,
128
- repository: string,
129
- modelId: string,
130
- pathToScreenShot: string,
131
- variation: VariationSM
132
- ): Promise<VariationSM> {
133
- const imageUrl = await sendVariationPreviewToS3(
134
- acl,
135
- repository,
136
- modelId,
137
- variation.id,
138
- pathToScreenShot
139
- );
140
- if (!imageUrl) return variation;
141
-
142
- return {
143
- ...variation,
144
- imageUrl,
145
- };
146
- }
147
-
148
- async function addImageUrlsToVariations(
149
- acl: ALC,
150
- repository: string,
151
- modelId: string,
152
- screenshotPaths: ComponentInfo["screenshotPaths"],
153
- variations: Array<VariationSM>
154
- ): Promise<Array<VariationSM>> {
43
+ export async function updateSlicesWithScreenshots(
44
+ client: InitClient,
45
+ acl: Acl,
46
+ components: Array<Component>
47
+ ): Promise<Array<SliceSM>> {
155
48
  return Promise.all(
156
- variations.map(async (variation) => {
157
- const screenshot = screenshotPaths[variation.id];
158
- if (!screenshot || !screenshot.path) return variation;
49
+ components.map(async (component) => {
50
+ const { screenshotPaths, model } = component;
159
51
 
160
- return maybeAddImageUrlToVariation(
161
- acl,
162
- repository,
163
- modelId,
164
- screenshot.path,
165
- variation
52
+ const variationsUpdated: VariationSM[] = await Promise.all(
53
+ model.variations.map(async (variation) =>
54
+ updateVariationWithScreenshot(
55
+ client,
56
+ acl,
57
+ screenshotPaths,
58
+ model.id,
59
+ variation
60
+ )
61
+ )
166
62
  );
167
- })
168
- );
169
- }
170
-
171
- async function maybeUpdateModelVariationsWithImageUrl(
172
- acl: ALC,
173
- repository: string,
174
- component: Component
175
- ): Promise<SliceSM> {
176
- const { screenshotPaths, model } = component;
177
- const variations = await addImageUrlsToVariations(
178
- acl,
179
- repository,
180
- model.id,
181
- screenshotPaths,
182
- model.variations
183
- );
184
-
185
- return {
186
- ...model,
187
- variations,
188
- };
189
- }
190
63
 
191
- export async function addImageUrlsToModelVariations(
192
- acl: ALC,
193
- repository: string,
194
- components: Array<Component>
195
- ): Promise<Array<SliceSM>> {
196
- return Promise.all(
197
- components.map(async (component) =>
198
- maybeUpdateModelVariationsWithImageUrl(acl, repository, component)
199
- )
64
+ return {
65
+ ...model,
66
+ variations: variationsUpdated,
67
+ };
68
+ })
200
69
  );
201
70
  }
@@ -1,34 +1,32 @@
1
- import { Component } from "@slicemachine/core/build/models";
1
+ import { Component, Manifest, Slices } from "@slicemachine/core/build/models";
2
+ import { Acl, ClientError } from "@slicemachine/client";
3
+ import { InitClient, logs } from "../../utils";
2
4
  import * as Libraries from "@slicemachine/core/build/libraries";
3
- import { logs } from "../../utils";
4
- import { getRemoteSliceIds, sendManyModelsToPrismic } from "./communication";
5
- import { getEndpointsFromBase } from "./endpoints";
6
5
  import { promptToPushSlices } from "./prompts";
7
- import { addImageUrlsToModelVariations, createAcl } from "./s3";
8
-
9
- export async function sendSlicesFromStarter(
10
- base: string,
11
- repository: string,
12
- authorization: string,
13
- libraryPaths: Array<string>,
14
- cwd: string
15
- ) {
16
- const endpoints = getEndpointsFromBase(base);
17
- const libraries = Libraries.libraries(cwd, libraryPaths);
18
-
19
- if (libraries.length === 0) return Promise.resolve(false);
20
-
21
- const remoteSlices = await getRemoteSliceIds(
22
- endpoints.Models,
23
- repository,
24
- authorization
25
- );
6
+ import { updateSlicesWithScreenshots } from "./s3";
7
+ import { writeError } from "../../utils/logs";
26
8
 
27
- if (remoteSlices.length) {
28
- // do prompt about slices
9
+ export async function sendSlices(
10
+ client: InitClient,
11
+ cwd: string,
12
+ manifest: Manifest
13
+ ): Promise<boolean> {
14
+ if (!manifest.libraries) return Promise.resolve(false); // No libraries defined
29
15
 
30
- const pushAnyway = await promptToPushSlices();
16
+ const libraries = Libraries.libraries(cwd, manifest.libraries);
17
+ const components = libraries.reduce<Array<Component>>((acc, lib) => {
18
+ return [...acc, ...lib.components];
19
+ }, []);
20
+
21
+ if (components.length === 0) return Promise.resolve(false); // No slices to send found in the libraries
31
22
 
23
+ const remoteSlicesIds: string[] = await client
24
+ .getSlices()
25
+ .then((slices) => slices.map((slice) => slice.id));
26
+
27
+ // If the repository already has Slices, ask the user to confirm.
28
+ if (remoteSlicesIds.length) {
29
+ const pushAnyway = await promptToPushSlices();
32
30
  if (pushAnyway === false) return Promise.resolve(true);
33
31
  }
34
32
 
@@ -37,25 +35,40 @@ export async function sendSlicesFromStarter(
37
35
  );
38
36
  spinner.start();
39
37
 
40
- const acl = await createAcl(endpoints.AclProvider, repository, authorization);
38
+ const acl: Acl | null = await client
39
+ .createAcl()
40
+ .catch((error: ClientError) => {
41
+ writeError(
42
+ "Uploading screenshots for your slices failed, please contact us."
43
+ );
44
+ writeError(error.message, "Full error:");
45
+ return null;
46
+ });
41
47
 
42
- const components = libraries.reduce<Array<Component>>((acc, lib) => {
43
- return [...acc, ...lib.components];
44
- }, []);
48
+ // If the acl failed to be created, don't mind the screenshots.
49
+ const models = acl
50
+ ? await updateSlicesWithScreenshots(client, acl, components)
51
+ : components.map((component) => component.model);
45
52
 
46
- const models = await addImageUrlsToModelVariations(
47
- acl,
48
- repository,
49
- components
50
- );
53
+ await Promise.all(
54
+ models.map(async (model) => {
55
+ const slice = Slices.fromSM(model);
51
56
 
52
- await sendManyModelsToPrismic(
53
- repository,
54
- authorization,
55
- endpoints.Models,
56
- remoteSlices,
57
- models
58
- );
57
+ const promise = remoteSlicesIds.includes(slice.id)
58
+ ? client.updateSlice(slice)
59
+ : client.insertSlice(slice);
60
+
61
+ return promise.catch((error: ClientError) => {
62
+ logs.writeError(`Sending slice ${model.id} - ${error.message}`);
63
+
64
+ // throwing the error again to stop the Promise.all
65
+ throw error;
66
+ });
67
+ })
68
+ ).catch(() => {
69
+ // the error about the slice that failed to be pushed should be in the terminal already.
70
+ process.exit(1);
71
+ });
59
72
 
60
73
  spinner.succeed();
61
74
  return Promise.resolve(true);
@@ -81,13 +81,17 @@ const authenticationHandler =
81
81
  };
82
82
  };
83
83
 
84
+ function stripTrailingSlash(str: string) {
85
+ return str.endsWith("/") ? str.slice(0, -1) : str;
86
+ }
87
+
84
88
  function buildServer(base: string, port: number, host: string): hapi.Server {
85
89
  const server = hapi.server({
86
90
  port,
87
91
  host,
88
92
  routes: {
89
93
  cors: {
90
- origin: [base],
94
+ origin: [stripTrailingSlash(base)],
91
95
  headers: ["Origin", "X-Requested-With", "Content-Type", "Accept"],
92
96
  },
93
97
  },
@@ -1,26 +1,30 @@
1
- import { Utils, Models } from "@slicemachine/core";
1
+ import { Utils } from "@slicemachine/core";
2
2
  import { startServerAndOpenBrowser } from "./helpers";
3
3
  import {
4
- Communication,
5
4
  Endpoints,
6
5
  PrismicSharedConfigManager,
7
6
  } from "@slicemachine/core/build/prismic";
7
+ import { InitClient } from "../client";
8
8
 
9
9
  async function startAuth({
10
- base,
10
+ client,
11
11
  url,
12
12
  action,
13
13
  }: {
14
- base: string;
14
+ client: InitClient;
15
15
  url: string;
16
16
  action: "signup" | "login";
17
17
  }): Promise<void> {
18
- const { onLoginFail } = await startServerAndOpenBrowser(url, action, base);
18
+ const { onLoginFail } = await startServerAndOpenBrowser(
19
+ url,
20
+ action,
21
+ client.apisEndpoints.Wroom
22
+ );
19
23
  try {
20
24
  // We wait 3 minutes before timeout
21
- await Utils.Poll.startPolling<Models.UserInfo | null, Models.UserInfo>(
22
- () => Auth.validateSession(base),
23
- (user): user is Models.UserInfo => !!user,
25
+ await Utils.Poll.startPolling<boolean, boolean>(
26
+ () => Auth.validateSession(client),
27
+ (isSessionValid): isSessionValid is boolean => isSessionValid == true,
24
28
  3000,
25
29
  60
26
30
  );
@@ -31,33 +35,29 @@ async function startAuth({
31
35
  }
32
36
 
33
37
  export const Auth = {
34
- login: async (base: string): Promise<void> => {
35
- const endpoints = Endpoints.buildEndpoints(base);
38
+ login: async (client: InitClient): Promise<void> => {
39
+ const endpoints = Endpoints.buildEndpoints(client.apisEndpoints.Wroom);
36
40
  return startAuth({
37
- base,
41
+ client,
38
42
  url: endpoints.Dashboard.cliLogin,
39
43
  action: "login",
40
44
  });
41
45
  },
42
- signup: async (base: string): Promise<void> => {
43
- const endpoints = Endpoints.buildEndpoints(base);
44
- return startAuth({
45
- base,
46
- url: endpoints.Dashboard.cliSignup,
47
- action: "signup",
48
- });
49
- },
50
- logout: (): void => PrismicSharedConfigManager.remove(),
51
- validateSession: async (
52
- requiredBase: string
53
- ): Promise<Models.UserInfo | null> => {
54
- const config = PrismicSharedConfigManager.get();
46
+ validateSession: async (client: InitClient): Promise<boolean> => {
47
+ const authToken = PrismicSharedConfigManager.getAuth();
55
48
 
56
- if (!config.cookies.length) return Promise.resolve(null); // default config, logged out.
57
- if (requiredBase != config.base) return Promise.resolve(null); // not the same base so it doesn't count.
58
-
59
- return Communication.validateSession(config.cookies, requiredBase).catch(
60
- () => null
61
- );
49
+ // verify token is by retrieving the profile, update the config if need be.
50
+ return client
51
+ .updateAuthenticationToken(authToken)
52
+ .profile()
53
+ .then((userProfile) => {
54
+ // settings shortId and IntercomHash as we switch between dev and prod.
55
+ PrismicSharedConfigManager.setProperties({
56
+ shortId: userProfile.shortId,
57
+ intercomHash: userProfile.intercomHash,
58
+ });
59
+ return true;
60
+ })
61
+ .catch(() => false);
62
62
  },
63
63
  };