@stackbit/dev 0.0.54 → 0.0.56-alpha.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,23 +1,34 @@
1
1
  import _ from 'lodash';
2
2
  import path from 'path';
3
- import ssg from './ssg';
4
- import Editor from '@stackbit/dev-common/dist/runner/editor';
5
- import GitCMS from '@stackbit/dev-common/dist/cms/git/git';
6
- import { Contentful as ContentfulCMS } from '@stackbit/dev-common/dist/cms/contentful/contentful';
7
- import SanityCMS from '@stackbit/dev-common/dist/cms/sanity/sanity';
8
- import Worker from '@stackbit/dev-common/dist/utils/worker';
9
- import { StackbitYaml } from '@stackbit/dev-common/dist/services/stackbit-yaml';
10
- import socketService from '@stackbit/dev-common/dist/services/socket-service';
3
+
4
+ import { Editor, StackbitYaml, ContentStoreAdapter, Worker } from '@stackbit/dev-common';
5
+ import { ContentStoreTypes } from '@stackbit/cms-core';
11
6
 
12
7
  import { watchDir } from '../services/file-watcher';
13
8
  import logger from '../services/logger';
14
9
  import consts from '../consts';
10
+ import ssg from './ssg';
15
11
  import { logAnnotationErrors } from '../services/annotation-errors';
12
+ import {
13
+ APIMethodCreateDocument,
14
+ APIMethodCreatePreset,
15
+ APIMethodDeleteDocument,
16
+ APIMethodDeletePreset,
17
+ APIMethodDuplicateDocument,
18
+ APIMethodGetAssets,
19
+ APIMethodUpdateDocument,
20
+ APIMethodUploadAssets,
21
+ APIMethodValidateDocuments,
22
+ APIMethodPublishDocuments,
23
+ APIMethodCreateAndLinkDocument,
24
+ APIMethodUploadAndLinkAsset
25
+ } from '../types/api-methods';
16
26
 
17
27
  interface RunnerOptions {
18
28
  rootDir: string;
19
29
  runnableDir?: string;
20
30
  cmsType?: string;
31
+ csiEnabled?: boolean;
21
32
  contentfulAccessToken?: string;
22
33
  contentfulSpaceIds?: [string];
23
34
  contentfulPreviewTokens?: [string];
@@ -28,131 +39,90 @@ interface RunnerOptions {
28
39
  sanityStudioUrl?: string;
29
40
  }
30
41
  export default class Runner {
31
- private editor: any;
32
- private cmsType?: string;
33
- private cms: any;
34
- private stackbitYaml: any;
35
- private workers: any;
42
+ private readonly stackbitYaml: StackbitYaml;
43
+ private readonly contentStoreAdapter: ContentStoreAdapter;
44
+ private readonly editor: Editor;
45
+ private readonly workers: { gitApp: Worker };
46
+ private readonly rootDir: string;
47
+ private readonly appDir: string;
36
48
  private watcher: any;
37
49
  private ssg: any;
38
50
 
39
- private readonly options: RunnerOptions;
40
- private rootDir: string;
41
- private appDir?: string;
42
-
43
51
  constructor(options: RunnerOptions) {
44
- this.options = options;
45
52
  this.rootDir = options.rootDir;
46
53
  this.appDir = options.runnableDir ? path.join(this.rootDir, options.runnableDir) : this.rootDir;
47
- this.cmsType = options.cmsType;
48
54
 
49
55
  this.workers = {
50
56
  gitApp: new Worker()
51
57
  };
52
- }
53
58
 
54
- async install() {
55
59
  this.stackbitYaml = new StackbitYaml({
56
60
  repoDir: this.rootDir,
57
61
  appDir: this.appDir,
58
- logger,
62
+ logger: logger,
59
63
  userLogger: logger,
60
64
  envName: 'local' //TODO
61
65
  });
62
- await this.stackbitYaml.load();
63
- this.cms = this.createCms();
64
- await this.cms.run();
65
- this.editor = new Editor(null, this.cms, this.stackbitYaml, {
66
- logger,
66
+
67
+ this.contentStoreAdapter = new ContentStoreAdapter({
68
+ cmsType: options.cmsType!,
69
+ csiEnabled: options.csiEnabled,
70
+ stackbitYaml: this.stackbitYaml,
71
+ workers: this.workers,
72
+ rootDir: this.rootDir,
73
+ projectDir: this.rootDir,
74
+ appDir: this.appDir,
75
+ logger: logger,
76
+ userLogger: logger,
77
+ localDev: true,
78
+ contentfulAccessToken: options.contentfulAccessToken,
79
+ contentfulSpaces: options.contentfulSpaceIds?.map((spaceId, i) => ({
80
+ spaceId,
81
+ previewToken: options.contentfulPreviewTokens?.[i]!
82
+ })),
83
+ sanityProject: {
84
+ projectId: options.sanityProjectId!,
85
+ token: options.sanityToken!,
86
+ studioPath: options.sanityStudioPath!,
87
+ dataset: options.sanityDataset!,
88
+ projectUrl: options.sanityStudioUrl!
89
+ },
90
+ assetIdPrefix: consts.ASSET_ID_PREFIX,
91
+ staticAssetsFilePath: consts.STATIC_ASSETS_FILE_PATH,
92
+ staticAssetsPublicPath: consts.STATIC_ASSETS_PUBLIC_PATH,
93
+ staticThemeAssetsFilePath: consts.STATIC_THEME_ASSETS_FILE_PATH,
94
+ staticThemeAssetsPublicPath: consts.STATIC_THEME_ASSETS_PUBLIC_PATH,
95
+ defaultContentTypeExtensions: consts.DEFAULT_CONTENT_TYPE_EXTENSIONS
96
+ });
97
+
98
+ this.editor = new Editor({
99
+ contentStoreAdapter: this.contentStoreAdapter,
100
+ stackbitYaml: this.stackbitYaml,
101
+ logger: logger,
67
102
  userLogger: logger,
68
103
  stackbitUrlPathField: consts.STACKBIT_URL_PATH_FIELD
69
104
  });
105
+ }
106
+
107
+ async install() {
108
+ await this.stackbitYaml.load();
109
+
110
+ await this.contentStoreAdapter.init();
111
+
70
112
  this.watcher = watchDir(this.rootDir, (filePaths) => {
71
113
  logger.debug(`File change detected: ${filePaths}`);
72
- this.handleFileChanges(filePaths).catch((err) => {
73
- logger.debug('Handle file change error', { err, filePaths });
74
- logger.error('Error handling file changes: ' + filePaths.join(', '));
75
- });
114
+ this.handleFileChanges(filePaths);
76
115
  });
77
116
  this.ssg = _.assign({}, ssg, this.stackbitYaml.internalStackbitRunnerOptions);
78
117
  }
79
118
 
80
- createCms() {
81
- let result;
82
- if (this.cmsType === 'contentful') {
83
- result = new ContentfulCMS({
84
- accessToken: this.options.contentfulAccessToken ?? null,
85
- spaces: this.options.contentfulSpaceIds!.map((spaceId, i) => ({
86
- spaceId,
87
- previewToken: this.options.contentfulPreviewTokens![i]
88
- })),
89
- stackbitYaml: this.stackbitYaml,
90
- encodeDelimiter: '',
91
- rootDir: this.rootDir,
92
- appDir: this.appDir!,
93
- repoDir: this.rootDir,
94
- staticThemeAssetsFilePath: consts.STATIC_THEME_ASSETS_FILE_PATH,
95
- staticThemeAssetsPublicPath: consts.STATIC_THEME_ASSETS_PUBLIC_PATH,
96
- watchForContentUpdates: true,
97
- onContentUpdate: () => {},
98
- logger: logger,
99
- userLogger: logger
100
- });
101
- } else if (this.cmsType === 'sanity') {
102
- result = new SanityCMS({
103
- rootDir: this.rootDir,
104
- repoDir: this.rootDir,
105
- appDir: this.appDir,
106
- stackbitYaml: this.stackbitYaml,
107
- encodeDelimiter: '',
108
- workers: this.workers,
109
- project: {
110
- projectId: this.options.sanityProjectId,
111
- token: this.options.sanityToken,
112
- studioPath: this.options.sanityStudioPath,
113
- dataset: this.options.sanityDataset,
114
- projectUrl: this.options.sanityStudioUrl
115
- },
116
- staticAssetsFilePath: consts.STATIC_ASSETS_FILE_PATH,
117
- staticAssetsPublicPath: consts.STATIC_ASSETS_PUBLIC_PATH,
118
- staticThemeAssetsFilePath: consts.STATIC_THEME_ASSETS_FILE_PATH,
119
- staticThemeAssetsPublicPath: consts.STATIC_THEME_ASSETS_PUBLIC_PATH,
120
- defaultContentTypeExtensions: consts.DEFAULT_CONTENT_TYPE_EXTENSIONS,
121
- onContentUpdate: () => {},
122
- logger,
123
- userLogger: logger
124
- });
125
- } else {
126
- result = new GitCMS({
127
- rootDir: this.rootDir,
128
- repoDir: this.rootDir,
129
- appDir: this.appDir,
130
- stackbitYaml: this.stackbitYaml,
131
- workers: this.workers,
132
- assetIdPrefix: consts.ASSET_ID_PREFIX,
133
- staticAssetsFilePath: consts.STATIC_ASSETS_FILE_PATH,
134
- staticAssetsPublicPath: consts.STATIC_ASSETS_PUBLIC_PATH,
135
- staticThemeAssetsFilePath: consts.STATIC_THEME_ASSETS_FILE_PATH,
136
- staticThemeAssetsPublicPath: consts.STATIC_THEME_ASSETS_PUBLIC_PATH,
137
- defaultContentTypeExtensions: consts.DEFAULT_CONTENT_TYPE_EXTENSIONS,
138
- logger,
139
- userLogger: logger
140
- });
141
- }
142
- return result;
143
- }
144
-
145
- async handleFileChanges(filePaths: string[]) {
146
- const pullResult = await this.cms.postPull(filePaths.map((filePath) => path.join(this.rootDir, filePath))).catch((err: any) => {
147
- logger.debug('Error in postPull', { err });
148
- throw err;
149
- });
150
- if (pullResult?.schemaChanged) {
151
- await this.stackbitYaml.load().catch((err: any) => {
152
- logger.debug('Error handling stackbit.yaml change', { err });
153
- logger.error('Error reloading stackbit.yaml');
154
- });
155
- socketService.notifySchemaChange();
119
+ async handleFileChanges(filePaths: string[]): Promise<void> {
120
+ try {
121
+ logger.debug('handleFileChanges', { filePaths });
122
+ await this.contentStoreAdapter.handleFileChanges(filePaths);
123
+ } catch (err) {
124
+ logger.debug('Handle file change error', { err, filePaths });
125
+ logger.error('Error handling file changes: ' + filePaths.join(', '));
156
126
  }
157
127
  }
158
128
 
@@ -177,7 +147,7 @@ export default class Runner {
177
147
  return this.ssg.directChangeSocketOrigin;
178
148
  }
179
149
 
180
- async getObjects({ objectIds = [], locale }: { objectIds: string[]; locale?: string }) {
150
+ async getObjects({ objectIds = [], locale }: { objectIds: string[]; locale?: string }): Promise<{ objects: ContentStoreTypes.APIObject[] }> {
181
151
  return this.editor.getObjects({ objectIds, locale });
182
152
  }
183
153
 
@@ -203,59 +173,107 @@ export default class Runner {
203
173
  }
204
174
 
205
175
  async getSiteMap() {
206
- return this.editor.getSiteMap(this.cms.getEncodingResult());
176
+ return this.editor.getSiteMap();
207
177
  }
208
178
 
209
179
  async getSchema({ locale }: { locale?: string }) {
210
180
  return this.editor.getSchema({ locale });
211
181
  }
212
182
 
213
- async getUrl(objectId: string, projectId: string, locale: string): Promise<any> {
214
- return this.editor.getUrl(objectId, projectId, locale);
183
+ async getUrl(srcDocumentId: string, srcProjectId: string, srcType: string, locale: string): Promise<string | null> {
184
+ return this.editor.getUrl(srcDocumentId, srcProjectId, srcType, locale);
215
185
  }
216
186
 
217
187
  async getObject(objectId: string, projectId: string): Promise<any> {
218
- return this.cms.getObject(objectId, projectId);
188
+ return this.contentStoreAdapter.getObject_deprecated({ objectId, projectId });
219
189
  }
220
190
 
221
191
  async listAssets(filterParams: any): Promise<any> {
222
- return this.cms.listAssets(filterParams);
192
+ return this.contentStoreAdapter.listAssets_deprecated(filterParams);
223
193
  }
224
194
 
225
195
  async uploadAsset(url: string, filename: string, user: any): Promise<any> {
226
- return this.cms.uploadAsset(url, filename, user);
196
+ return this.contentStoreAdapter.uploadAsset_deprecated({ url, fileName: filename, user });
197
+ }
198
+
199
+ async createDocument(data: APIMethodCreateDocument['data']): Promise<APIMethodCreateDocument['result']> {
200
+ return this.contentStoreAdapter.createDocument(data);
201
+ }
202
+
203
+ async createAndLinkDocument(data: APIMethodCreateAndLinkDocument['data']): Promise<APIMethodCreateAndLinkDocument['result']> {
204
+ return this.contentStoreAdapter.createAndLinkDocument(data);
205
+ }
206
+
207
+ async uploadAndLinkAsset(data: APIMethodUploadAndLinkAsset['data']): Promise<APIMethodUploadAndLinkAsset['result']> {
208
+ return this.contentStoreAdapter.uploadAndLinkAsset(data);
209
+ }
210
+
211
+ async duplicateDocument(data: APIMethodDuplicateDocument['data']): Promise<APIMethodDuplicateDocument['result']> {
212
+ return this.contentStoreAdapter.duplicateDocument(data);
213
+ }
214
+
215
+ async updateDocument(data: APIMethodUpdateDocument['data']): Promise<APIMethodUpdateDocument['result']> {
216
+ return this.contentStoreAdapter.updateDocument(data);
217
+ }
218
+
219
+ async deleteDocument(data: APIMethodDeleteDocument['data']): Promise<APIMethodDeleteDocument['result']> {
220
+ return this.contentStoreAdapter.deleteDocument(data);
221
+ }
222
+
223
+ async validateDocuments(data: APIMethodValidateDocuments['data']): Promise<APIMethodValidateDocuments['result']> {
224
+ return this.contentStoreAdapter.validateDocuments(data);
225
+ }
226
+
227
+ async publishDocuments(data: APIMethodPublishDocuments['data']): Promise<APIMethodPublishDocuments['result']> {
228
+ return this.contentStoreAdapter.publishDocuments(data);
229
+ }
230
+
231
+ async getAssets(data: APIMethodGetAssets['data']): Promise<APIMethodGetAssets['result']> {
232
+ return this.contentStoreAdapter.getApiAssets(data);
233
+ }
234
+
235
+ async uploadAssets(data: APIMethodUploadAssets['data']): Promise<APIMethodUploadAssets['result']> {
236
+ return this.contentStoreAdapter.uploadAssets(data);
237
+ }
238
+
239
+ async createPreset(data: APIMethodCreatePreset['data']): Promise<APIMethodCreatePreset['result']> {
240
+ return this.editor.createPreset(this.appDir, data.fieldDataPath, _.omit(data, ['fieldDataPath', 'user', 'dryRun']) as any, data.user, data.dryRun);
241
+ }
242
+
243
+ async deletePreset(data: APIMethodDeletePreset['data']): Promise<APIMethodDeletePreset['result']> {
244
+ return this.editor.deletePreset(this.appDir, data.presetId, data.user);
227
245
  }
228
246
 
229
247
  async makeAction(action: string, data: any): Promise<any> {
230
248
  switch (action) {
231
249
  case 'createPage':
232
- return this.cms.createObject(data);
250
+ return this.contentStoreAdapter.createObject_deprecated(data);
233
251
  case 'duplicatePage':
234
- return this.cms.duplicateObject(data);
252
+ return this.contentStoreAdapter.duplicateObject_deprecated(data);
235
253
  case 'updatePage':
236
- return this.cms.updateObject(data.changedFields);
254
+ return this.contentStoreAdapter.updateObject_deprecated(data);
237
255
  case 'deleteObject':
238
- return this.cms.deleteObject(data.srcObjectId, data.srcProjectId);
256
+ return this.contentStoreAdapter.deleteObject_deprecated(data);
239
257
  case 'getAssets':
240
- const pageId = data.pageId || 1;
241
- return this.cms.listAssets(data).then((result: any) => ({
242
- data: result.assets,
243
- meta: {
244
- nextPage: pageId < result.meta.totalPages ? pageId + 1 : null
245
- }
246
- }));
258
+ return this.contentStoreAdapter.getAssets_deprecated(data);
247
259
  case 'uploadAssets':
248
- return this.cms.uploadAssets(data.assets);
260
+ return this.contentStoreAdapter.uploadAssets(data);
249
261
  case 'createPreset':
250
- return this.editor.createPreset(this.appDir, data.fieldDataPath, _.omit(data, ['fieldDataPath', 'user', 'dryRun']), data.user, data.dryRun);
262
+ return this.editor.createPreset(
263
+ this.appDir,
264
+ data.fieldDataPath,
265
+ _.omit(data, ['fieldDataPath', 'user', 'dryRun']) as any,
266
+ data.user,
267
+ data.dryRun
268
+ );
251
269
  case 'deletePreset':
252
270
  return this.editor.deletePreset(this.appDir, data.presetId, data.user);
253
271
  default:
254
- throw new Error('Unsupported Operation: ' + action);
272
+ throw new Error('Unsupported Operation: ' + data.action);
255
273
  }
256
274
  }
257
275
 
258
276
  keepAlive() {
259
- _.invoke(this.cms, 'keepAlive');
277
+ this.contentStoreAdapter.keepAlive();
260
278
  }
261
279
  }
@@ -19,6 +19,7 @@ interface ServerOptions {
19
19
  runnableDir?: string;
20
20
  uuid: string;
21
21
  cmsType?: string;
22
+ csiEnabled?: boolean;
22
23
  contentfulAccessToken?: string;
23
24
  contentfulSpaceIds?: [string];
24
25
  contentfulPreviewTokens?: [string];
@@ -38,7 +39,7 @@ export async function start({ serverPort, ssgPort, ssgHost, rootDir, runnableDir
38
39
  logger.debug('Done runner init');
39
40
 
40
41
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
41
-
42
+
42
43
  const server = express();
43
44
  const httpServer = http.createServer(server);
44
45
  socketService.use(httpServer, logger);
@@ -4,6 +4,7 @@ import bodyParser from 'body-parser';
4
4
  import Runner from '../runner';
5
5
  import logger from '../services/logger';
6
6
  import { PageNotFoundError } from '@stackbit/dev-common/dist/services/response-errors';
7
+ import { APIMethod, APIMethodDataForMethod, APIMethodName, APIMethodResultForMethod } from '../types/api-methods';
7
8
 
8
9
  const authMiddleware = (uuid: string) => {
9
10
  return (req: Request, res: Response, next: NextFunction) => {
@@ -109,9 +110,11 @@ export default function routes(router: Router, runner: Runner, uuid: string) {
109
110
  });
110
111
  });
111
112
  router.get('/_pageUrl', (req: Request, res: Response, next: NextFunction) => {
112
- const { entryId: objectId, spaceId: projectId, locale } = req.query;
113
+ const { srcType, locale } = req.query;
114
+ const srcProjectId = req.query.srcProjectId ?? req.query.spaceId;
115
+ const srcDocumentId = req.query.srcDocumentId ?? req.query.entryId;
113
116
  runner
114
- .getUrl(objectId as string, projectId as string, locale as string)
117
+ .getUrl(srcDocumentId as string, srcProjectId as string, srcType as string, locale as string)
115
118
  .then((url) => res.json({ url }))
116
119
  .catch((err) => {
117
120
  if (err !== PageNotFoundError) {
@@ -155,6 +158,7 @@ export default function routes(router: Router, runner: Runner, uuid: string) {
155
158
  router.get('/_health', (req: Request, res: Response) => {
156
159
  return res.status(200).json({ status: 'ok' });
157
160
  });
161
+
158
162
  router.post('/_action', [authMiddleware(uuid), bodyParser.json({ limit: '50mb' })], (req: Request, res: Response, next: NextFunction) => {
159
163
  const { action, data } = req.body;
160
164
  return runner
@@ -168,8 +172,32 @@ export default function routes(router: Router, runner: Runner, uuid: string) {
168
172
  if (_.has(err, 'stack')) {
169
173
  logger.debug('error.stack: ' + err.stack);
170
174
  }
171
- res.status(500).send('Error');
175
+ res.status(500).json({ error: err.message || err });
172
176
  next(err);
173
177
  });
174
178
  });
179
+
180
+ router.post(
181
+ '/_stackbit/:method',
182
+ [authMiddleware(uuid), bodyParser.json({ limit: '50mb' })],
183
+ <T extends APIMethodName>(req: Request<{ method: T }>, res: Response, next: NextFunction) => {
184
+ const method: APIMethodName = req.params.method;
185
+ if (!runner[method]) {
186
+ logger.error(`method ${method} is not supported`);
187
+ }
188
+ const data = req.body;
189
+ return runner[method](data)
190
+ .then((result: APIMethod['result']) => {
191
+ res.json(result);
192
+ })
193
+ .catch((err) => {
194
+ logger.error(`error executing ${method} method`, { data, error: err.message || err });
195
+ if (_.has(err, 'stack')) {
196
+ logger.debug('error.stack: ' + err.stack);
197
+ }
198
+ res.status(500).send({ error: err.message || err });
199
+ next(err);
200
+ });
201
+ }
202
+ );
175
203
  }
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import _ from 'lodash';
3
3
  import path from 'path';
4
4
 
5
- export async function logAnnotationErrors(errors: any[], logger: any, baseDir: string, sourcemaps?: { [k: string]: any }) {
5
+ export function logAnnotationErrors(errors: any[], logger: any, baseDir: string, sourcemaps?: { [k: string]: any }) {
6
6
  if (_.isEmpty(errors)) {
7
7
  return;
8
8
  }
@@ -14,7 +14,7 @@ const logger = winston.createLogger({
14
14
  transports: transports
15
15
  });
16
16
 
17
- export const createLogger = ({ label }: { label: string }) => {
17
+ export const createLogger = ({ label }: { label: string }): winston.Logger => {
18
18
  const formats = defaultFormats.slice();
19
19
  if (label) {
20
20
  formats.push(
@@ -24,11 +24,21 @@ export const createLogger = ({ label }: { label: string }) => {
24
24
  })
25
25
  );
26
26
  }
27
- return winston.createLogger({
27
+ const logger = winston.createLogger({
28
28
  level: config.logLevel,
29
29
  format: winston.format.combine(...formats),
30
30
  transports: transports
31
31
  });
32
+ logger.createLogger = createLogger;
33
+ return logger;
32
34
  };
33
35
 
36
+ declare module "winston" {
37
+ interface Logger {
38
+ createLogger: ({ label }: { label: string }) => winston.Logger
39
+ }
40
+ }
41
+
42
+ logger.createLogger = createLogger;
43
+
34
44
  export default logger;