@plasmicapp/cli 0.1.166 → 0.1.170

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.
@@ -249,7 +249,10 @@ class PlasmicApi {
249
249
  iconChecksums: [],
250
250
  globalVariantChecksums: [],
251
251
  projectCssChecksum: "",
252
+ globalContextsChecksum: "",
252
253
  },
254
+ usedNpmPackages: [],
255
+ externalCssImports: [],
253
256
  };
254
257
  return result;
255
258
  });
@@ -274,6 +277,11 @@ class PlasmicApi {
274
277
  throw new Error("Unimplemented");
275
278
  });
276
279
  }
280
+ latestCodegenVersion() {
281
+ return __awaiter(this, void 0, void 0, function* () {
282
+ return "0.0.1";
283
+ });
284
+ }
277
285
  requiredPackages() {
278
286
  return __awaiter(this, void 0, void 0, function* () {
279
287
  return {
@@ -51,20 +51,20 @@ describe("Project API tokens", () => {
51
51
  fixtures_1.tmpRepo.writePlasmicJson(Object.assign(Object.assign({}, fixtures_1.defaultPlasmicJson), { projects: [Object.assign(Object.assign({}, fixtures_1.project1Config), { projectApiToken: "blah" })] }));
52
52
  yield expect(sync_1.sync(fixtures_1.opts)).resolves.toBeUndefined();
53
53
  fixtures_1.expectProject1Components();
54
- fixtures_1.expectProject1PlasmicJson();
54
+ fixtures_1.expectProject1PlasmicJson({ projectApiToken: true });
55
55
  // Re-run, this time with no auth.
56
56
  removeAuth();
57
- yield expect(sync_1.sync(fixtures_1.opts)).resolves.toBeUndefined();
57
+ yield expect(sync_1.sync(fixtures_1.opts)).rejects.toThrow("No user+token, and project API tokens don't match");
58
58
  }));
59
59
  test("is filled in by auth'd user if project exists but token was initially missing", () => __awaiter(void 0, void 0, void 0, function* () {
60
60
  fixtures_1.opts.projects = ["projectId1"];
61
61
  fixtures_1.tmpRepo.writePlasmicJson(Object.assign(Object.assign({}, fixtures_1.defaultPlasmicJson), { projects: [fixtures_1.project1Config] }));
62
62
  yield expect(sync_1.sync(fixtures_1.opts)).resolves.toBeUndefined();
63
63
  fixtures_1.expectProject1Components();
64
- fixtures_1.expectProject1PlasmicJson();
64
+ fixtures_1.expectProject1PlasmicJson({ projectApiToken: true });
65
65
  // Re-run, this time with no auth.
66
66
  removeAuth();
67
- yield expect(sync_1.sync(fixtures_1.opts)).resolves.toBeUndefined();
67
+ yield expect(sync_1.sync(fixtures_1.opts)).rejects.toThrow("Unable to authenticate Plasmic. Please run 'plasmic auth' or check the projectApiTokens in your plasmic.json, and try again.");
68
68
  }));
69
69
  test("when not available, should prompt for auth", () => __awaiter(void 0, void 0, void 0, function* () {
70
70
  fixtures_1.opts.projects = ["projectId1"];
@@ -0,0 +1,4 @@
1
+ import { ChecksumBundle, ProjectMetaBundle } from "../api";
2
+ import { PlasmicContext, ProjectConfig, ProjectLock } from "../utils/config-utils";
3
+ export declare function syncGlobalContexts(context: PlasmicContext, projectMeta: ProjectMetaBundle, projectConfig: ProjectConfig, projectLock: ProjectLock, checksums: ChecksumBundle, baseDir: string): Promise<void>;
4
+ export declare function getGlobalContextsResourcePath(context: PlasmicContext, projectConfig: ProjectConfig): string;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.getGlobalContextsResourcePath = exports.syncGlobalContexts = void 0;
16
+ const deps_1 = require("../deps");
17
+ const code_utils_1 = require("../utils/code-utils");
18
+ const lodash_1 = __importDefault(require("lodash"));
19
+ const file_utils_1 = require("../utils/file-utils");
20
+ const COMPONENT_NAME = "PlasmicGlobalContextsProvider";
21
+ function syncGlobalContexts(context, projectMeta, projectConfig, projectLock, checksums, baseDir) {
22
+ return __awaiter(this, void 0, void 0, function* () {
23
+ const resourcePath = getGlobalContextsResourcePath(context, projectConfig);
24
+ if (checksums.globalContextsChecksum && projectMeta.globalContextBundle) {
25
+ if (context.cliArgs.quiet !== true) {
26
+ deps_1.logger.info(`Syncing component: ${COMPONENT_NAME}@${projectLock.version}\t['${projectConfig.projectName}' ${projectConfig.projectId} ${projectConfig.version}]`);
27
+ }
28
+ if (context.config.code.lang === "js") {
29
+ projectMeta.globalContextBundle.contextModule = code_utils_1.formatScript(code_utils_1.tsxToJsx(projectMeta.globalContextBundle.contextModule), baseDir);
30
+ }
31
+ file_utils_1.writeFileContent(context, resourcePath, projectMeta.globalContextBundle.contextModule, { force: false });
32
+ projectConfig.globalContextsFilePath = resourcePath;
33
+ const fl = projectLock.fileLocks.find((fl) => fl.assetId === projectConfig.projectId && fl.type === "globalContexts");
34
+ if (fl) {
35
+ fl.checksum = checksums.globalContextsChecksum;
36
+ }
37
+ else {
38
+ projectLock.fileLocks.push({
39
+ assetId: projectConfig.projectId,
40
+ checksum: checksums.globalContextsChecksum,
41
+ type: "globalContexts",
42
+ });
43
+ }
44
+ }
45
+ else if (!checksums.globalContextsChecksum &&
46
+ !projectMeta.globalContextBundle) {
47
+ if (file_utils_1.fileExists(context, resourcePath)) {
48
+ file_utils_1.deleteFile(context, resourcePath);
49
+ }
50
+ projectConfig.globalContextsFilePath = "";
51
+ lodash_1.default.remove(projectLock.fileLocks, (fl) => fl.assetId === projectConfig.projectId && fl.type === "globalContexts");
52
+ }
53
+ });
54
+ }
55
+ exports.syncGlobalContexts = syncGlobalContexts;
56
+ function getGlobalContextsResourcePath(context, projectConfig) {
57
+ return projectConfig.globalContextsFilePath !== ""
58
+ ? projectConfig.globalContextsFilePath
59
+ : file_utils_1.defaultResourcePath(context, projectConfig, `${COMPONENT_NAME}.${context.config.code.lang === "ts" ? "tsx" : "jsx"}`);
60
+ }
61
+ exports.getGlobalContextsResourcePath = getGlobalContextsResourcePath;
@@ -54,6 +54,7 @@ const sync_global_variants_1 = require("./sync-global-variants");
54
54
  const sync_icons_1 = require("./sync-icons");
55
55
  const sync_images_1 = require("./sync-images");
56
56
  const sync_styles_1 = require("./sync-styles");
57
+ const sync_global_contexts_1 = require("./sync-global-contexts");
57
58
  function ensureRequiredPackages(context, baseDir, yes) {
58
59
  return __awaiter(this, void 0, void 0, function* () {
59
60
  const requireds = yield context.api.requiredPackages();
@@ -208,13 +209,15 @@ function sync(opts, metadataDefaults) {
208
209
  ...versionResolution.dependencies,
209
210
  ].map((p) => lodash_1.default.pick(p, "projectId", "projectApiToken"));
210
211
  context.api.attachProjectIdsAndTokens(projectIdsAndTokens);
212
+ const externalNpmPackages = new Set();
213
+ const externalCssImports = new Set();
211
214
  // Perform the actual sync
212
215
  yield file_utils_1.withBufferedFs(() => __awaiter(this, void 0, void 0, function* () {
213
216
  var _b;
214
217
  // Sync in sequence (no parallelism)
215
218
  // going in reverse to get leaves of the dependency tree first
216
219
  for (const projectMeta of projectsToSync) {
217
- yield syncProject(context, opts, projectIdsAndTokens, projectMeta.projectId, projectMeta.componentIds, projectMeta.version, projectMeta.dependencies, summary, pendingMerge, projectMeta.indirect, metadataDefaults);
220
+ yield syncProject(context, opts, projectIdsAndTokens, projectMeta.projectId, projectMeta.componentIds, projectMeta.version, projectMeta.dependencies, summary, pendingMerge, projectMeta.indirect, externalNpmPackages, externalCssImports, metadataDefaults);
218
221
  }
219
222
  // Materialize scheme into each component config.
220
223
  context.config.projects.forEach((p) => p.components.forEach((c) => {
@@ -255,9 +258,22 @@ function sync(opts, metadataDefaults) {
255
258
  const config = Object.assign(Object.assign({}, loaderConfig), { projects: lodash_1.default.sortBy(lodash_1.default.uniqBy([...freshIdsAndTokens, ...((_b = loaderConfig === null || loaderConfig === void 0 ? void 0 : loaderConfig.projects) !== null && _b !== void 0 ? _b : [])], (p) => p.projectId), (p) => p.projectId) });
256
259
  writeLoaderConfig(opts, config);
257
260
  }
261
+ const codegenVersion = yield context.api.latestCodegenVersion();
262
+ context.lock.projects.forEach((p) => {
263
+ if (projectsToSync.some((syncedProject) => syncedProject.projectId === p.projectId)) {
264
+ p.codegenVersion = codegenVersion;
265
+ }
266
+ });
258
267
  // Write the new ComponentConfigs to disk
259
268
  yield config_utils_1.updateConfig(context, context.config, baseDir);
260
269
  }));
270
+ yield checkExternalPkgs(context, baseDir, opts, Array.from(externalNpmPackages.keys()));
271
+ if (!opts.quiet && externalCssImports.size > 0) {
272
+ deps_1.logger.info(`This project uses external packages and styles. Make sure to import the following global CSS: ` +
273
+ Array.from(externalCssImports.keys())
274
+ .map((stmt) => `"${stmt}"`)
275
+ .join(", "));
276
+ }
261
277
  // Post-sync commands
262
278
  if (!opts.ignorePostSync) {
263
279
  for (const cmd of context.config.postSyncCommands || []) {
@@ -272,6 +288,20 @@ function sync(opts, metadataDefaults) {
272
288
  });
273
289
  }
274
290
  exports.sync = sync;
291
+ function checkExternalPkgs(context, baseDir, opts, pkgs) {
292
+ return __awaiter(this, void 0, void 0, function* () {
293
+ const missingPkgs = pkgs.filter((pkg) => {
294
+ const installedPkg = npm_utils_1.findInstalledVersion(context, baseDir, pkg);
295
+ return !installedPkg;
296
+ });
297
+ if (missingPkgs.length > 0) {
298
+ const upgrade = yield user_utils_1.confirmWithUser(`The following packages aren't installed but are required by some projects, would you like to install them? ${missingPkgs.join(", ")}`, opts.yes);
299
+ if (upgrade) {
300
+ npm_utils_1.installUpgrade(missingPkgs.join(" "), baseDir);
301
+ }
302
+ }
303
+ });
304
+ }
275
305
  function maybeRenamePathExt(context, path, ext) {
276
306
  if (!path) {
277
307
  return path;
@@ -292,7 +322,7 @@ function fixFileExtension(context) {
292
322
  });
293
323
  });
294
324
  }
295
- function syncProject(context, opts, projectIdsAndTokens, projectId, componentIds, projectVersion, dependencies, summary, pendingMerge, indirect, metadataDefaults) {
325
+ function syncProject(context, opts, projectIdsAndTokens, projectId, componentIds, projectVersion, dependencies, summary, pendingMerge, indirect, externalNpmPackages, externalCssImports, metadataDefaults) {
296
326
  var _a;
297
327
  return __awaiter(this, void 0, void 0, function* () {
298
328
  const newComponentScheme = opts.newComponentScheme || context.config.code.scheme;
@@ -336,6 +366,8 @@ function syncProject(context, opts, projectIdsAndTokens, projectId, componentIds
336
366
  yield sync_styles_1.upsertStyleTokens(context, projectBundle.usedTokens);
337
367
  yield sync_icons_1.syncProjectIconAssets(context, projectId, projectVersion, projectBundle.iconAssets, projectBundle.checksums, opts.baseDir);
338
368
  yield sync_images_1.syncProjectImageAssets(context, projectId, projectVersion, projectBundle.imageAssets, projectBundle.checksums);
369
+ (projectBundle.usedNpmPackages || []).forEach((pkg) => externalNpmPackages.add(pkg));
370
+ (projectBundle.externalCssImports || []).forEach((css) => externalCssImports.add(css));
339
371
  });
340
372
  }
341
373
  function syncStyleConfig(context, response) {
@@ -365,7 +397,6 @@ function syncProjectConfig(context, projectBundle, projectApiToken, version, dep
365
397
  if (!projectConfig.cssFilePath) {
366
398
  projectConfig.cssFilePath = defaultCssFilePath;
367
399
  }
368
- projectConfig.projectApiToken = projectApiToken;
369
400
  // plasmic.lock
370
401
  const projectLock = config_utils_1.getOrAddProjectLock(context, projectConfig.projectId);
371
402
  projectLock.version = version;
@@ -414,6 +445,7 @@ function syncProjectConfig(context, projectBundle, projectApiToken, version, dep
414
445
  projectConfig.jsBundleThemes.length === 0) {
415
446
  delete projectConfig.jsBundleThemes;
416
447
  }
448
+ yield sync_global_contexts_1.syncGlobalContexts(context, projectBundle, projectConfig, projectLock, checksums, baseDir);
417
449
  // Write out components
418
450
  yield sync_components_1.syncProjectComponents(context, projectConfig, version, componentBundles, forceOverwrite, appendJsxOnMissingBase, summary, pendingMerge, projectLock, checksums, baseDir);
419
451
  });
package/dist/api.d.ts CHANGED
@@ -25,6 +25,10 @@ export interface GlobalVariantBundle {
25
25
  contextModule: string;
26
26
  contextFileName: string;
27
27
  }
28
+ export interface GlobalContextBundle {
29
+ id: string;
30
+ contextModule: string;
31
+ }
28
32
  export interface JsBundleTheme {
29
33
  themeFileName: string;
30
34
  themeModule: string;
@@ -36,6 +40,7 @@ export interface ProjectMetaBundle {
36
40
  cssFileName: string;
37
41
  cssRules: string;
38
42
  jsBundleThemes?: JsBundleTheme[];
43
+ globalContextBundle?: GlobalContextBundle;
39
44
  }
40
45
  export interface IconBundle {
41
46
  id: string;
@@ -80,6 +85,8 @@ export interface ProjectBundle {
80
85
  iconAssets: IconBundle[];
81
86
  imageAssets: ImageBundle[];
82
87
  checksums: ChecksumBundle;
88
+ usedNpmPackages: string[];
89
+ externalCssImports: string[];
83
90
  }
84
91
  export declare type ProjectMeta = Omit<ProjectBundle, "projectConfig">;
85
92
  export interface StyleConfigResponse {
@@ -109,6 +116,7 @@ export interface ChecksumBundle {
109
116
  iconChecksums: Array<[string, string]>;
110
117
  globalVariantChecksums: Array<[string, string]>;
111
118
  projectCssChecksum: string;
119
+ globalContextsChecksum: string;
112
120
  }
113
121
  export interface CodeComponentMeta {
114
122
  id: string;
@@ -125,6 +133,7 @@ export interface ProjectIdAndToken {
125
133
  }
126
134
  export declare class PlasmicApi {
127
135
  private auth;
136
+ private codegenVersion?;
128
137
  constructor(auth: AuthConfig);
129
138
  genStyleConfig(styleOpts?: StyleConfig): Promise<StyleConfigResponse>;
130
139
  /**
@@ -144,6 +153,7 @@ export declare class PlasmicApi {
144
153
  }[], recursive?: boolean): Promise<VersionResolution>;
145
154
  getCurrentUser(): Promise<import("axios").AxiosResponse<any>>;
146
155
  requiredPackages(): Promise<RequiredPackages>;
156
+ latestCodegenVersion(): Promise<string>;
147
157
  /**
148
158
  * Code-gen endpoint.
149
159
  * This will fetch components at an exact specified version.
package/dist/api.js CHANGED
@@ -66,6 +66,15 @@ class PlasmicApi {
66
66
  return Object.assign({}, resp.data);
67
67
  });
68
68
  }
69
+ latestCodegenVersion() {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ if (!this.codegenVersion) {
72
+ const resp = yield this.post(`${this.codegenHost}/api/v1/code/latest-codegen-version`);
73
+ this.codegenVersion = resp.data;
74
+ }
75
+ return this.codegenVersion;
76
+ });
77
+ }
69
78
  /**
70
79
  * Code-gen endpoint.
71
80
  * This will fetch components at an exact specified version.
package/dist/index.js CHANGED
File without changes
@@ -282,6 +282,10 @@
282
282
  "description": "File location for the project-wide css styles. Relative to srcDir",
283
283
  "type": "string"
284
284
  },
285
+ "globalContextsFilePath": {
286
+ "description": "File location for the project-wide global contexts. Relative to srcDir",
287
+ "type": "string"
288
+ },
285
289
  "icons": {
286
290
  "description": "Metadata for each synced icon in this project",
287
291
  "items": {
@@ -326,6 +330,7 @@
326
330
  "required": [
327
331
  "components",
328
332
  "cssFilePath",
333
+ "globalContextsFilePath",
329
334
  "icons",
330
335
  "images",
331
336
  "indirect",
@@ -9,5 +9,7 @@ export declare function standardTestSetup(includeDep?: boolean): void;
9
9
  export declare function standardTestTeardown(): void;
10
10
  export declare function expectProject1Components(): void;
11
11
  export declare const project1Config: ProjectConfig;
12
- export declare function expectProject1PlasmicJson(): void;
12
+ export declare function expectProject1PlasmicJson(optional?: {
13
+ [k in keyof ProjectConfig]?: boolean;
14
+ }): void;
13
15
  export declare function expectProjectAndDepPlasmicJson(): void;
@@ -139,12 +139,15 @@ exports.project1Config = {
139
139
  images: [],
140
140
  jsBundleThemes: [],
141
141
  indirect: false,
142
+ globalContextsFilePath: "",
142
143
  };
143
- function expectProject1PlasmicJson() {
144
+ function expectProject1PlasmicJson(optional) {
144
145
  const plasmicJson = exports.tmpRepo.readPlasmicJson();
145
146
  expect(plasmicJson.projects.length).toEqual(1);
146
147
  const projectConfig = plasmicJson.projects[0];
147
- expect(projectConfig.projectApiToken).toBe("abc");
148
+ if (!(optional === null || optional === void 0 ? void 0 : optional.projectApiToken)) {
149
+ expect(projectConfig.projectApiToken).toBe("abc");
150
+ }
148
151
  expect(projectConfig.components.length).toEqual(2);
149
152
  const componentNames = projectConfig.components.map((c) => c.name);
150
153
  expect(componentNames).toContain("Button");
@@ -13,6 +13,7 @@ function getChecksums(context, opts, projectId, componentIds) {
13
13
  cssRulesChecksums: [],
14
14
  globalVariantChecksums: [],
15
15
  projectCssChecksum: "",
16
+ globalContextsChecksum: "",
16
17
  };
17
18
  }
18
19
  const fileLocks = projectLock.fileLocks;
@@ -51,6 +52,11 @@ function getChecksums(context, opts, projectId, componentIds) {
51
52
  const projectCssChecksums = fileLocks.filter((fileLock) => fileLock.type === "projectCss");
52
53
  lang_utils_1.assert(projectCssChecksums.length < 2);
53
54
  const projectCssChecksum = projectCssChecksums.length > 0 ? projectCssChecksums[0].checksum : "";
55
+ const globalContextsChecksums = fileLocks.filter((fileLock) => fileLock.type === "globalContexts" && fileLock.assetId === projectId);
56
+ lang_utils_1.assert(globalContextsChecksums.length < 2);
57
+ const globalContextsChecksum = globalContextsChecksums.length > 0
58
+ ? globalContextsChecksums[0].checksum
59
+ : "";
54
60
  return {
55
61
  imageChecksums,
56
62
  iconChecksums,
@@ -58,6 +64,7 @@ function getChecksums(context, opts, projectId, componentIds) {
58
64
  cssRulesChecksums,
59
65
  globalVariantChecksums,
60
66
  projectCssChecksum,
67
+ globalContextsChecksum,
61
68
  };
62
69
  }
63
70
  exports.getChecksums = getChecksums;
@@ -41,9 +41,11 @@ const Prettier = __importStar(require("prettier"));
41
41
  const prettier_1 = require("prettier");
42
42
  const ts = __importStar(require("typescript"));
43
43
  const upath_1 = __importDefault(require("upath"));
44
+ const sync_global_contexts_1 = require("../actions/sync-global-contexts");
44
45
  const sync_images_1 = require("../actions/sync-images");
45
46
  const deps_1 = require("../deps");
46
47
  const error_1 = require("../utils/error");
48
+ const config_utils_1 = require("./config-utils");
47
49
  const file_utils_1 = require("./file-utils");
48
50
  const lang_utils_1 = require("./lang-utils");
49
51
  exports.formatAsLocal = (content, filePath, baseDir, defaultOpts = {}) => {
@@ -341,6 +343,7 @@ function fixAllImportStatements(context, baseDir, summary) {
341
343
  }
342
344
  }
343
345
  }
346
+ fixGlobalContextImportStatements(context, fixImportContext, baseDir);
344
347
  });
345
348
  }
346
349
  exports.fixAllImportStatements = fixAllImportStatements;
@@ -455,3 +458,26 @@ exports.formatScript = (code, baseDir) => {
455
458
  useTabs: false,
456
459
  });
457
460
  };
461
+ function fixGlobalContextImportStatements(context, fixImportContext, baseDir) {
462
+ return __awaiter(this, void 0, void 0, function* () {
463
+ for (const project of context.config.projects) {
464
+ if (!project.globalContextsFilePath)
465
+ continue;
466
+ const resourcePath = sync_global_contexts_1.getGlobalContextsResourcePath(context, project);
467
+ let prevContent;
468
+ try {
469
+ prevContent = file_utils_1.readFileText(file_utils_1.makeFilePath(context, resourcePath)).toString();
470
+ }
471
+ catch (e) {
472
+ deps_1.logger.warn(`${resourcePath} is missing. If you deleted this component, remember to remove the component from ${config_utils_1.CONFIG_FILE_NAME}`);
473
+ throw e;
474
+ }
475
+ const newContent = replaceImports(context, prevContent, resourcePath, fixImportContext, false, baseDir, true);
476
+ if (prevContent !== newContent) {
477
+ yield file_utils_1.writeFileContent(context, resourcePath, newContent, {
478
+ force: true,
479
+ });
480
+ }
481
+ }
482
+ });
483
+ }
@@ -118,6 +118,8 @@ export interface ProjectConfig {
118
118
  version: string;
119
119
  /** File location for the project-wide css styles. Relative to srcDir */
120
120
  cssFilePath: string;
121
+ /** File location for the project-wide global contexts. Relative to srcDir */
122
+ globalContextsFilePath: string;
121
123
  jsBundleThemes?: JsBundleThemeConfig[];
122
124
  codeComponents?: CodeComponentConfig[];
123
125
  /** Metadata for each synced component in this project. */
@@ -213,7 +215,7 @@ export interface GlobalVariantGroupConfig {
213
215
  contextFilePath: string;
214
216
  }
215
217
  export interface FileLock {
216
- type: "renderModule" | "cssRules" | "icon" | "image" | "projectCss" | "globalVariant";
218
+ type: "renderModule" | "cssRules" | "icon" | "image" | "projectCss" | "globalVariant" | "globalContexts";
217
219
  checksum: string;
218
220
  assetId: string;
219
221
  }
@@ -225,6 +227,7 @@ export interface ProjectLock {
225
227
  };
226
228
  lang: "ts" | "js";
227
229
  fileLocks: FileLock[];
230
+ codegenVersion?: string;
228
231
  }
229
232
  export interface PlasmicLock {
230
233
  projects: ProjectLock[];
@@ -40,6 +40,7 @@ function createProjectConfig(base) {
40
40
  icons: [],
41
41
  images: [],
42
42
  indirect: base.indirect,
43
+ globalContextsFilePath: "",
43
44
  };
44
45
  }
45
46
  exports.createProjectConfig = createProjectConfig;
@@ -150,6 +151,7 @@ function getOrAddProjectConfig(context, projectId, base // if one doesn't exist,
150
151
  images: [],
151
152
  jsBundleThemes: [],
152
153
  indirect: false,
154
+ globalContextsFilePath: "",
153
155
  };
154
156
  context.config.projects.push(project);
155
157
  }
@@ -188,6 +188,7 @@ function getAllPaths(context) {
188
188
  };
189
189
  const pushProject = (proj) => {
190
190
  pushPath(proj, "cssFilePath");
191
+ pushPath(proj, "globalContextsFilePath");
191
192
  for (const component of proj.components) {
192
193
  pushComponent(component);
193
194
  }
@@ -81,10 +81,7 @@ function removeMissingFilesFromLock(context, config, lock) {
81
81
  image.id,
82
82
  image,
83
83
  ]));
84
- const knownIcons = Object.fromEntries(knownProjects[project.projectId].icons.map((icons) => [
85
- icons.id,
86
- icons,
87
- ]));
84
+ const knownIcons = Object.fromEntries(knownProjects[project.projectId].icons.map((icons) => [icons.id, icons]));
88
85
  project.fileLocks = project.fileLocks.filter((lock) => {
89
86
  switch (lock.type) {
90
87
  default:
@@ -100,6 +97,8 @@ function removeMissingFilesFromLock(context, config, lock) {
100
97
  return knownImages[lock.assetId];
101
98
  case "icon":
102
99
  return knownIcons[lock.assetId];
100
+ case "globalContexts":
101
+ return knownProjects[project.projectId].globalContextsFilePath;
103
102
  }
104
103
  });
105
104
  return project;
@@ -169,6 +168,11 @@ function resolveMissingFilesInConfig(context, config) {
169
168
  for (const project of config.projects) {
170
169
  project.cssFilePath =
171
170
  (yield attemptToRestoreFilePath(context, project.cssFilePath, baseNameToFiles)) || "";
171
+ if (!project.globalContextsFilePath) {
172
+ project.globalContextsFilePath = "";
173
+ }
174
+ project.globalContextsFilePath =
175
+ (yield attemptToRestoreFilePath(context, project.globalContextsFilePath, baseNameToFiles)) || "";
172
176
  project.images = yield filterFiles(project.images, "filePath");
173
177
  project.icons = yield filterFiles(project.icons, "moduleFilePath");
174
178
  project.jsBundleThemes = yield filterFiles(project.jsBundleThemes || [], "themeFilePath");
@@ -75,6 +75,15 @@ function checkProjectMeta(meta, root, context, opts) {
75
75
  const projectName = meta.projectName;
76
76
  const newVersion = meta.version;
77
77
  const indirect = meta.indirect;
78
+ // If the codegen version on-disk is invalid, we will sync again the project.
79
+ const checkCodegenVersion = () => __awaiter(this, void 0, void 0, function* () {
80
+ const projectLock = context.lock.projects.find((p) => p.projectId === projectId);
81
+ if (!!(projectLock === null || projectLock === void 0 ? void 0 : projectLock.codegenVersion) && semver.gte(projectLock.codegenVersion, yield context.api.latestCodegenVersion())) {
82
+ return false;
83
+ }
84
+ return true;
85
+ });
86
+ const isOnDiskCodeInvalid = yield checkCodegenVersion();
78
87
  // Checks newVersion against plasmic.lock
79
88
  const checkVersionLock = () => __awaiter(this, void 0, void 0, function* () {
80
89
  const projectLock = context.lock.projects.find((p) => p.projectId === projectId);
@@ -88,7 +97,9 @@ function checkProjectMeta(meta, root, context, opts) {
88
97
  meta !== root) {
89
98
  // If this is a dependency (not root), and we're dealing with latest dep version
90
99
  // just skip, it's confusing
91
- deps_1.logger.warn(`'${root.projectName}' depends on ${projectName}@${newVersion}. To update this project, explicitly specify this project for sync. Skipping...`);
100
+ if (!isOnDiskCodeInvalid) {
101
+ deps_1.logger.warn(`'${root.projectName}' depends on ${projectName}@${newVersion}. To update this project, explicitly specify this project for sync. Skipping...`);
102
+ }
92
103
  return false;
93
104
  }
94
105
  if (semver.isLatest(newVersion)) {
@@ -106,7 +117,9 @@ function checkProjectMeta(meta, root, context, opts) {
106
117
  return true;
107
118
  }
108
119
  else {
109
- deps_1.logger.info(`Project '${projectName}'@${newVersion} is already up to date; skipping. (To force an update, run again with "--force")`);
120
+ if (!isOnDiskCodeInvalid) {
121
+ deps_1.logger.info(`Project '${projectName}'@${newVersion} is already up to date; skipping. (To force an update, run again with "--force")`);
122
+ }
110
123
  return false;
111
124
  }
112
125
  }
@@ -164,9 +177,24 @@ function checkProjectMeta(meta, root, context, opts) {
164
177
  // we should always sync it, even if nothing has changed
165
178
  return true;
166
179
  }
167
- return ((yield checkVersionLock()) &&
180
+ const checkedVersion = (yield checkVersionLock()) &&
168
181
  (yield checkVersionRange()) &&
169
- (yield checkIndirect()));
182
+ (yield checkIndirect());
183
+ if (!checkedVersion && isOnDiskCodeInvalid) {
184
+ // sync, but try to keep the current version on disk
185
+ const projectLock = context.lock.projects.find((p) => p.projectId === projectId);
186
+ const versionOnDisk = projectLock === null || projectLock === void 0 ? void 0 : projectLock.version;
187
+ deps_1.logger.warn(`Project '${projectName}' was synced by an incompatible version of Plasmic Codegen. Syncing again on the same version ${projectName}@${versionOnDisk}`);
188
+ meta.version = versionOnDisk !== null && versionOnDisk !== void 0 ? versionOnDisk : meta.version;
189
+ return true;
190
+ }
191
+ else if (checkedVersion) {
192
+ // sync and upgrade the version
193
+ return true;
194
+ }
195
+ else {
196
+ return false;
197
+ }
170
198
  });
171
199
  }
172
200
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasmicapp/cli",
3
- "version": "0.1.166",
3
+ "version": "0.1.170",
4
4
  "description": "plasmic cli for syncing local code with Plasmic designs",
5
5
  "engines": {
6
6
  "node": ">=12"
@@ -339,7 +339,10 @@ class PlasmicApi {
339
339
  iconChecksums: [],
340
340
  globalVariantChecksums: [],
341
341
  projectCssChecksum: "",
342
+ globalContextsChecksum: "",
342
343
  } as ChecksumBundle,
344
+ usedNpmPackages: [],
345
+ externalCssImports: [],
343
346
  };
344
347
  return result;
345
348
  }
@@ -370,6 +373,10 @@ class PlasmicApi {
370
373
  throw new Error("Unimplemented");
371
374
  }
372
375
 
376
+ async latestCodegenVersion(): Promise<string> {
377
+ return "0.0.1";
378
+ }
379
+
373
380
  async requiredPackages(): Promise<RequiredPackages> {
374
381
  return {
375
382
  "@plasmicapp/loader": "0.0.1",
@@ -71,11 +71,13 @@ describe("Project API tokens", () => {
71
71
 
72
72
  expectProject1Components();
73
73
 
74
- expectProject1PlasmicJson();
74
+ expectProject1PlasmicJson({ projectApiToken: true });
75
75
 
76
76
  // Re-run, this time with no auth.
77
77
  removeAuth();
78
- await expect(sync(opts)).resolves.toBeUndefined();
78
+ await expect(sync(opts)).rejects.toThrow(
79
+ "No user+token, and project API tokens don't match"
80
+ );
79
81
  });
80
82
 
81
83
  test("is filled in by auth'd user if project exists but token was initially missing", async () => {
@@ -88,11 +90,13 @@ describe("Project API tokens", () => {
88
90
 
89
91
  expectProject1Components();
90
92
 
91
- expectProject1PlasmicJson();
93
+ expectProject1PlasmicJson({ projectApiToken: true });
92
94
 
93
95
  // Re-run, this time with no auth.
94
96
  removeAuth();
95
- await expect(sync(opts)).resolves.toBeUndefined();
97
+ await expect(sync(opts)).rejects.toThrow(
98
+ "Unable to authenticate Plasmic. Please run 'plasmic auth' or check the projectApiTokens in your plasmic.json, and try again."
99
+ );
96
100
  });
97
101
 
98
102
  test("when not available, should prompt for auth", async () => {
@@ -0,0 +1,87 @@
1
+ import { ChecksumBundle, ProjectMetaBundle } from "../api";
2
+ import { logger } from "../deps";
3
+ import { formatScript, tsxToJsx } from "../utils/code-utils";
4
+ import L from "lodash";
5
+ import {
6
+ PlasmicContext,
7
+ ProjectConfig,
8
+ ProjectLock,
9
+ } from "../utils/config-utils";
10
+ import {
11
+ defaultResourcePath,
12
+ deleteFile,
13
+ fileExists,
14
+ writeFileContent,
15
+ } from "../utils/file-utils";
16
+
17
+ const COMPONENT_NAME = "PlasmicGlobalContextsProvider";
18
+
19
+ export async function syncGlobalContexts(
20
+ context: PlasmicContext,
21
+ projectMeta: ProjectMetaBundle,
22
+ projectConfig: ProjectConfig,
23
+ projectLock: ProjectLock,
24
+ checksums: ChecksumBundle,
25
+ baseDir: string
26
+ ) {
27
+ const resourcePath = getGlobalContextsResourcePath(context, projectConfig);
28
+ if (checksums.globalContextsChecksum && projectMeta.globalContextBundle) {
29
+ if (context.cliArgs.quiet !== true) {
30
+ logger.info(
31
+ `Syncing component: ${COMPONENT_NAME}@${projectLock.version}\t['${projectConfig.projectName}' ${projectConfig.projectId} ${projectConfig.version}]`
32
+ );
33
+ }
34
+ if (context.config.code.lang === "js") {
35
+ projectMeta.globalContextBundle.contextModule = formatScript(
36
+ tsxToJsx(projectMeta.globalContextBundle.contextModule),
37
+ baseDir
38
+ );
39
+ }
40
+ writeFileContent(
41
+ context,
42
+ resourcePath,
43
+ projectMeta.globalContextBundle.contextModule,
44
+ { force: false }
45
+ );
46
+ projectConfig.globalContextsFilePath = resourcePath;
47
+ const fl = projectLock.fileLocks.find(
48
+ (fl) =>
49
+ fl.assetId === projectConfig.projectId && fl.type === "globalContexts"
50
+ );
51
+ if (fl) {
52
+ fl.checksum = checksums.globalContextsChecksum;
53
+ } else {
54
+ projectLock.fileLocks.push({
55
+ assetId: projectConfig.projectId,
56
+ checksum: checksums.globalContextsChecksum,
57
+ type: "globalContexts",
58
+ });
59
+ }
60
+ } else if (
61
+ !checksums.globalContextsChecksum &&
62
+ !projectMeta.globalContextBundle
63
+ ) {
64
+ if (fileExists(context, resourcePath)) {
65
+ deleteFile(context, resourcePath);
66
+ }
67
+ projectConfig.globalContextsFilePath = "";
68
+ L.remove(
69
+ projectLock.fileLocks,
70
+ (fl) =>
71
+ fl.assetId === projectConfig.projectId && fl.type === "globalContexts"
72
+ );
73
+ }
74
+ }
75
+
76
+ export function getGlobalContextsResourcePath(
77
+ context: PlasmicContext,
78
+ projectConfig: ProjectConfig
79
+ ) {
80
+ return projectConfig.globalContextsFilePath !== ""
81
+ ? projectConfig.globalContextsFilePath
82
+ : defaultResourcePath(
83
+ context,
84
+ projectConfig,
85
+ `${COMPONENT_NAME}.${context.config.code.lang === "ts" ? "tsx" : "jsx"}`
86
+ );
87
+ }
@@ -64,6 +64,7 @@ import { syncGlobalVariants } from "./sync-global-variants";
64
64
  import { syncProjectIconAssets } from "./sync-icons";
65
65
  import { syncProjectImageAssets } from "./sync-images";
66
66
  import { upsertStyleTokens } from "./sync-styles";
67
+ import { syncGlobalContexts } from "./sync-global-contexts";
67
68
 
68
69
  export interface SyncArgs extends CommonArgs {
69
70
  projects: readonly string[];
@@ -315,6 +316,8 @@ export async function sync(
315
316
  ].map((p) => L.pick(p, "projectId", "projectApiToken"));
316
317
 
317
318
  context.api.attachProjectIdsAndTokens(projectIdsAndTokens);
319
+ const externalNpmPackages = new Set<string>();
320
+ const externalCssImports = new Set<string>();
318
321
 
319
322
  // Perform the actual sync
320
323
  await withBufferedFs(async () => {
@@ -332,6 +335,8 @@ export async function sync(
332
335
  summary,
333
336
  pendingMerge,
334
337
  projectMeta.indirect,
338
+ externalNpmPackages,
339
+ externalCssImports,
335
340
  metadataDefaults
336
341
  );
337
342
  }
@@ -412,10 +417,36 @@ export async function sync(
412
417
  writeLoaderConfig(opts, config);
413
418
  }
414
419
 
420
+ const codegenVersion = await context.api.latestCodegenVersion();
421
+ context.lock.projects.forEach((p) => {
422
+ if (
423
+ projectsToSync.some(
424
+ (syncedProject) => syncedProject.projectId === p.projectId
425
+ )
426
+ ) {
427
+ p.codegenVersion = codegenVersion;
428
+ }
429
+ });
415
430
  // Write the new ComponentConfigs to disk
416
431
  await updateConfig(context, context.config, baseDir);
417
432
  });
418
433
 
434
+ await checkExternalPkgs(
435
+ context,
436
+ baseDir,
437
+ opts,
438
+ Array.from(externalNpmPackages.keys())
439
+ );
440
+
441
+ if (!opts.quiet && externalCssImports.size > 0) {
442
+ logger.info(
443
+ `This project uses external packages and styles. Make sure to import the following global CSS: ` +
444
+ Array.from(externalCssImports.keys())
445
+ .map((stmt) => `"${stmt}"`)
446
+ .join(", ")
447
+ );
448
+ }
449
+
419
450
  // Post-sync commands
420
451
  if (!opts.ignorePostSync) {
421
452
  for (const cmd of context.config.postSyncCommands || []) {
@@ -430,6 +461,30 @@ export async function sync(
430
461
  }
431
462
  }
432
463
 
464
+ async function checkExternalPkgs(
465
+ context: PlasmicContext,
466
+ baseDir: string,
467
+ opts: SyncArgs,
468
+ pkgs: string[]
469
+ ) {
470
+ const missingPkgs = pkgs.filter((pkg) => {
471
+ const installedPkg = findInstalledVersion(context, baseDir, pkg);
472
+ return !installedPkg;
473
+ });
474
+ if (missingPkgs.length > 0) {
475
+ const upgrade = await confirmWithUser(
476
+ `The following packages aren't installed but are required by some projects, would you like to install them? ${missingPkgs.join(
477
+ ", "
478
+ )}`,
479
+ opts.yes
480
+ );
481
+
482
+ if (upgrade) {
483
+ installUpgrade(missingPkgs.join(" "), baseDir);
484
+ }
485
+ }
486
+ }
487
+
433
488
  function maybeRenamePathExt(
434
489
  context: PlasmicContext,
435
490
  path: string,
@@ -480,6 +535,8 @@ async function syncProject(
480
535
  summary: Map<string, ComponentUpdateSummary>,
481
536
  pendingMerge: ComponentPendingMerge[],
482
537
  indirect: boolean,
538
+ externalNpmPackages: Set<string>,
539
+ externalCssImports: Set<string>,
483
540
  metadataDefaults?: Metadata
484
541
  ): Promise<void> {
485
542
  const newComponentScheme =
@@ -602,6 +659,12 @@ async function syncProject(
602
659
  projectBundle.imageAssets,
603
660
  projectBundle.checksums
604
661
  );
662
+ (projectBundle.usedNpmPackages || []).forEach((pkg) =>
663
+ externalNpmPackages.add(pkg)
664
+ );
665
+ (projectBundle.externalCssImports || []).forEach((css) =>
666
+ externalCssImports.add(css)
667
+ );
605
668
  }
606
669
 
607
670
  async function syncStyleConfig(
@@ -662,7 +725,6 @@ async function syncProjectConfig(
662
725
  if (!projectConfig.cssFilePath) {
663
726
  projectConfig.cssFilePath = defaultCssFilePath;
664
727
  }
665
- projectConfig.projectApiToken = projectApiToken;
666
728
 
667
729
  // plasmic.lock
668
730
  const projectLock = getOrAddProjectLock(context, projectConfig.projectId);
@@ -730,6 +792,15 @@ async function syncProjectConfig(
730
792
  delete projectConfig.jsBundleThemes;
731
793
  }
732
794
 
795
+ await syncGlobalContexts(
796
+ context,
797
+ projectBundle,
798
+ projectConfig,
799
+ projectLock,
800
+ checksums,
801
+ baseDir
802
+ );
803
+
733
804
  // Write out components
734
805
  await syncProjectComponents(
735
806
  context,
package/src/api.ts CHANGED
@@ -39,6 +39,11 @@ export interface GlobalVariantBundle {
39
39
  contextFileName: string;
40
40
  }
41
41
 
42
+ export interface GlobalContextBundle {
43
+ id: string;
44
+ contextModule: string;
45
+ }
46
+
42
47
  export interface JsBundleTheme {
43
48
  themeFileName: string;
44
49
  themeModule: string;
@@ -51,6 +56,7 @@ export interface ProjectMetaBundle {
51
56
  cssFileName: string;
52
57
  cssRules: string;
53
58
  jsBundleThemes?: JsBundleTheme[];
59
+ globalContextBundle?: GlobalContextBundle;
54
60
  }
55
61
 
56
62
  export interface IconBundle {
@@ -101,6 +107,8 @@ export interface ProjectBundle {
101
107
  iconAssets: IconBundle[];
102
108
  imageAssets: ImageBundle[];
103
109
  checksums: ChecksumBundle;
110
+ usedNpmPackages: string[];
111
+ externalCssImports: string[];
104
112
  }
105
113
 
106
114
  export type ProjectMeta = Omit<ProjectBundle, "projectConfig">;
@@ -140,6 +148,8 @@ export interface ChecksumBundle {
140
148
  globalVariantChecksums: Array<[string, string]>;
141
149
  // Checksum of projectCss file
142
150
  projectCssChecksum: string;
151
+ // Checksum of project global contexts
152
+ globalContextsChecksum: string;
143
153
  }
144
154
 
145
155
  export interface CodeComponentMeta {
@@ -159,6 +169,7 @@ export interface ProjectIdAndToken {
159
169
  }
160
170
 
161
171
  export class PlasmicApi {
172
+ private codegenVersion?: string;
162
173
  constructor(private auth: AuthConfig) {}
163
174
 
164
175
  async genStyleConfig(styleOpts?: StyleConfig): Promise<StyleConfigResponse> {
@@ -211,6 +222,16 @@ export class PlasmicApi {
211
222
  return { ...resp.data } as RequiredPackages;
212
223
  }
213
224
 
225
+ async latestCodegenVersion(): Promise<string> {
226
+ if (!this.codegenVersion) {
227
+ const resp = await this.post(
228
+ `${this.codegenHost}/api/v1/code/latest-codegen-version`
229
+ );
230
+ this.codegenVersion = resp.data as string;
231
+ }
232
+ return this.codegenVersion;
233
+ }
234
+
214
235
  /**
215
236
  * Code-gen endpoint.
216
237
  * This will fetch components at an exact specified version.
@@ -148,13 +148,18 @@ export const project1Config: ProjectConfig = {
148
148
  images: [],
149
149
  jsBundleThemes: [],
150
150
  indirect: false,
151
+ globalContextsFilePath: "",
151
152
  };
152
153
 
153
- export function expectProject1PlasmicJson() {
154
+ export function expectProject1PlasmicJson(
155
+ optional?: { [k in keyof ProjectConfig]?: boolean }
156
+ ) {
154
157
  const plasmicJson = tmpRepo.readPlasmicJson();
155
158
  expect(plasmicJson.projects.length).toEqual(1);
156
159
  const projectConfig = plasmicJson.projects[0];
157
- expect(projectConfig.projectApiToken).toBe("abc");
160
+ if (!optional?.projectApiToken) {
161
+ expect(projectConfig.projectApiToken).toBe("abc");
162
+ }
158
163
  expect(projectConfig.components.length).toEqual(2);
159
164
  const componentNames = projectConfig.components.map((c) => c.name);
160
165
  expect(componentNames).toContain("Button");
@@ -25,6 +25,7 @@ export function getChecksums(
25
25
  cssRulesChecksums: [],
26
26
  globalVariantChecksums: [],
27
27
  projectCssChecksum: "",
28
+ globalContextsChecksum: "",
28
29
  };
29
30
  }
30
31
 
@@ -95,6 +96,16 @@ export function getChecksums(
95
96
  const projectCssChecksum =
96
97
  projectCssChecksums.length > 0 ? projectCssChecksums[0].checksum : "";
97
98
 
99
+ const globalContextsChecksums = fileLocks.filter(
100
+ (fileLock) =>
101
+ fileLock.type === "globalContexts" && fileLock.assetId === projectId
102
+ );
103
+ assert(globalContextsChecksums.length < 2);
104
+ const globalContextsChecksum =
105
+ globalContextsChecksums.length > 0
106
+ ? globalContextsChecksums[0].checksum
107
+ : "";
108
+
98
109
  return {
99
110
  imageChecksums,
100
111
  iconChecksums,
@@ -102,5 +113,6 @@ export function getChecksums(
102
113
  cssRulesChecksums,
103
114
  globalVariantChecksums,
104
115
  projectCssChecksum,
116
+ globalContextsChecksum,
105
117
  };
106
118
  }
@@ -8,6 +8,7 @@ import * as Prettier from "prettier";
8
8
  import { Options, resolveConfig } from "prettier";
9
9
  import * as ts from "typescript";
10
10
  import path from "upath";
11
+ import { getGlobalContextsResourcePath } from "../actions/sync-global-contexts";
11
12
  import {
12
13
  fixComponentCssReferences,
13
14
  fixComponentImagesReferences,
@@ -17,6 +18,7 @@ import { HandledError } from "../utils/error";
17
18
  import {
18
19
  CodeComponentConfig,
19
20
  ComponentConfig,
21
+ CONFIG_FILE_NAME,
20
22
  GlobalVariantGroupConfig,
21
23
  IconConfig,
22
24
  ImageConfig,
@@ -473,6 +475,7 @@ export async function fixAllImportStatements(
473
475
  }
474
476
  }
475
477
  }
478
+ fixGlobalContextImportStatements(context, fixImportContext, baseDir);
476
479
  }
477
480
 
478
481
  async function fixComponentImportStatements(
@@ -654,3 +657,42 @@ export const formatScript = (code: string, baseDir: string) => {
654
657
  useTabs: false,
655
658
  });
656
659
  };
660
+
661
+ async function fixGlobalContextImportStatements(
662
+ context: PlasmicContext,
663
+ fixImportContext: FixImportContext,
664
+ baseDir: string
665
+ ) {
666
+ for (const project of context.config.projects) {
667
+ if (!project.globalContextsFilePath) continue;
668
+ const resourcePath = getGlobalContextsResourcePath(context, project);
669
+
670
+ let prevContent: string;
671
+ try {
672
+ prevContent = readFileText(
673
+ makeFilePath(context, resourcePath)
674
+ ).toString();
675
+ } catch (e) {
676
+ logger.warn(
677
+ `${resourcePath} is missing. If you deleted this component, remember to remove the component from ${CONFIG_FILE_NAME}`
678
+ );
679
+ throw e;
680
+ }
681
+
682
+ const newContent = replaceImports(
683
+ context,
684
+ prevContent,
685
+ resourcePath,
686
+ fixImportContext,
687
+ false,
688
+ baseDir,
689
+ true
690
+ );
691
+
692
+ if (prevContent !== newContent) {
693
+ await writeFileContent(context, resourcePath, newContent, {
694
+ force: true,
695
+ });
696
+ }
697
+ }
698
+ }
@@ -159,6 +159,8 @@ export interface ProjectConfig {
159
159
  version: string;
160
160
  /** File location for the project-wide css styles. Relative to srcDir */
161
161
  cssFilePath: string;
162
+ /** File location for the project-wide global contexts. Relative to srcDir */
163
+ globalContextsFilePath: string;
162
164
 
163
165
  // Code-component-related fields can be treated as optional not to be shown
164
166
  // to the users nor appear to be missing in the documentation.
@@ -197,6 +199,7 @@ export function createProjectConfig(base: {
197
199
  icons: [],
198
200
  images: [],
199
201
  indirect: base.indirect,
202
+ globalContextsFilePath: "",
200
203
  };
201
204
  }
202
205
 
@@ -299,7 +302,8 @@ export interface FileLock {
299
302
  | "icon"
300
303
  | "image"
301
304
  | "projectCss"
302
- | "globalVariant";
305
+ | "globalVariant"
306
+ | "globalContexts";
303
307
  // The checksum value for the file
304
308
  checksum: string;
305
309
  // The component id, or the image asset id
@@ -318,6 +322,8 @@ export interface ProjectLock {
318
322
  lang: "ts" | "js";
319
323
  // One for each file whose checksum is computed
320
324
  fileLocks: FileLock[];
325
+ // The version of Codegen when this project was written
326
+ codegenVersion?: string;
321
327
  }
322
328
 
323
329
  export interface PlasmicLock {
@@ -528,6 +534,7 @@ export function getOrAddProjectConfig(
528
534
  images: [],
529
535
  jsBundleThemes: [],
530
536
  indirect: false,
537
+ globalContextsFilePath: "",
531
538
  };
532
539
  context.config.projects.push(project);
533
540
  }
@@ -242,6 +242,7 @@ function getAllPaths(context: PlasmicContext): BundleKeyPair[] {
242
242
 
243
243
  const pushProject = (proj: ProjectConfig) => {
244
244
  pushPath(proj, "cssFilePath");
245
+ pushPath(proj, "globalContextsFilePath");
245
246
  for (const component of proj.components) {
246
247
  pushComponent(component);
247
248
  }
@@ -282,7 +283,10 @@ function getAllPaths(context: PlasmicContext): BundleKeyPair[] {
282
283
  * Fixes all src-relative file paths in PlasmicConfig by detecting file
283
284
  * movement on disk.
284
285
  */
285
- export async function fixAllFilePaths(context: PlasmicContext, baseDir: string) {
286
+ export async function fixAllFilePaths(
287
+ context: PlasmicContext,
288
+ baseDir: string
289
+ ) {
286
290
  const baseNameToFiles = buildBaseNameToFiles(context);
287
291
  let changed = false;
288
292
 
@@ -79,10 +79,7 @@ function removeMissingFilesFromLock(
79
79
  ])
80
80
  );
81
81
  const knownIcons = Object.fromEntries(
82
- knownProjects[project.projectId].icons.map((icons) => [
83
- icons.id,
84
- icons,
85
- ])
82
+ knownProjects[project.projectId].icons.map((icons) => [icons.id, icons])
86
83
  );
87
84
 
88
85
  project.fileLocks = project.fileLocks.filter((lock) => {
@@ -100,6 +97,8 @@ function removeMissingFilesFromLock(
100
97
  return knownImages[lock.assetId];
101
98
  case "icon":
102
99
  return knownIcons[lock.assetId];
100
+ case "globalContexts":
101
+ return knownProjects[project.projectId].globalContextsFilePath;
103
102
  }
104
103
  });
105
104
 
@@ -209,6 +208,16 @@ async function resolveMissingFilesInConfig(
209
208
  baseNameToFiles
210
209
  )) || "";
211
210
 
211
+ if (!project.globalContextsFilePath) {
212
+ project.globalContextsFilePath = "";
213
+ }
214
+ project.globalContextsFilePath =
215
+ (await attemptToRestoreFilePath(
216
+ context,
217
+ project.globalContextsFilePath,
218
+ baseNameToFiles
219
+ )) || "";
220
+
212
221
  project.images = await filterFiles(project.images, "filePath");
213
222
  project.icons = await filterFiles(project.icons, "moduleFilePath");
214
223
  project.jsBundleThemes = await filterFiles(
@@ -68,6 +68,21 @@ async function checkProjectMeta(
68
68
  const newVersion = meta.version;
69
69
  const indirect = meta.indirect;
70
70
 
71
+ // If the codegen version on-disk is invalid, we will sync again the project.
72
+ const checkCodegenVersion = async (): Promise<boolean> => {
73
+ const projectLock = context.lock.projects.find(
74
+ (p) => p.projectId === projectId
75
+ );
76
+
77
+ if (!!projectLock?.codegenVersion && semver.gte(projectLock.codegenVersion, await context.api.latestCodegenVersion())) {
78
+ return false;
79
+ }
80
+
81
+ return true;
82
+ };
83
+
84
+ const isOnDiskCodeInvalid = await checkCodegenVersion();
85
+
71
86
  // Checks newVersion against plasmic.lock
72
87
  const checkVersionLock = async (): Promise<boolean> => {
73
88
  const projectLock = context.lock.projects.find(
@@ -87,9 +102,11 @@ async function checkProjectMeta(
87
102
  ) {
88
103
  // If this is a dependency (not root), and we're dealing with latest dep version
89
104
  // just skip, it's confusing
90
- logger.warn(
91
- `'${root.projectName}' depends on ${projectName}@${newVersion}. To update this project, explicitly specify this project for sync. Skipping...`
92
- );
105
+ if (!isOnDiskCodeInvalid) {
106
+ logger.warn(
107
+ `'${root.projectName}' depends on ${projectName}@${newVersion}. To update this project, explicitly specify this project for sync. Skipping...`
108
+ );
109
+ }
93
110
  return false;
94
111
  }
95
112
 
@@ -111,9 +128,11 @@ async function checkProjectMeta(
111
128
  );
112
129
  return true;
113
130
  } else {
114
- logger.info(
115
- `Project '${projectName}'@${newVersion} is already up to date; skipping. (To force an update, run again with "--force")`
116
- );
131
+ if (!isOnDiskCodeInvalid) {
132
+ logger.info(
133
+ `Project '${projectName}'@${newVersion} is already up to date; skipping. (To force an update, run again with "--force")`
134
+ );
135
+ }
117
136
  return false;
118
137
  }
119
138
  }
@@ -211,12 +230,29 @@ async function checkProjectMeta(
211
230
  // we should always sync it, even if nothing has changed
212
231
  return true;
213
232
  }
214
-
215
- return (
233
+ const checkedVersion =
216
234
  (await checkVersionLock()) &&
217
235
  (await checkVersionRange()) &&
218
- (await checkIndirect())
219
- );
236
+ (await checkIndirect());
237
+
238
+ if(!checkedVersion && isOnDiskCodeInvalid) {
239
+ // sync, but try to keep the current version on disk
240
+ const projectLock = context.lock.projects.find(
241
+ (p) => p.projectId === projectId
242
+ );
243
+ const versionOnDisk = projectLock?.version;
244
+ logger.warn(
245
+ `Project '${projectName}' was synced by an incompatible version of Plasmic Codegen. Syncing again on the same version ${projectName}@${versionOnDisk}`
246
+ );
247
+
248
+ meta.version = versionOnDisk ?? meta.version;
249
+ return true;
250
+ } else if (checkedVersion) {
251
+ // sync and upgrade the version
252
+ return true;
253
+ } else {
254
+ return false;
255
+ }
220
256
  }
221
257
 
222
258
  /**