@plasmicapp/cli 0.1.165 → 0.1.169

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.
@@ -50,7 +50,7 @@ function mockProjectToProjectVersionMeta(mock, componentIdOrNames) {
50
50
  .filter((c) => !componentIdOrNames ||
51
51
  componentIdOrNames.includes(c.name) ||
52
52
  componentIdOrNames.includes(c.id))
53
- .map((c) => c.id) });
53
+ .map((c) => c.id), indirect: false });
54
54
  }
55
55
  /**
56
56
  * Call this in test to setup the data model
@@ -250,6 +250,8 @@ class PlasmicApi {
250
250
  globalVariantChecksums: [],
251
251
  projectCssChecksum: "",
252
252
  },
253
+ usedNpmPackages: [],
254
+ externalCssImports: [],
253
255
  };
254
256
  return result;
255
257
  });
@@ -274,6 +276,12 @@ class PlasmicApi {
274
276
  throw new Error("Unimplemented");
275
277
  });
276
278
  }
279
+ latestCodegenVersion() {
280
+ return __awaiter(this, void 0, void 0, function* () {
281
+ return "0.0.1";
282
+ });
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"];
@@ -150,6 +150,7 @@ function sync(opts, metadataDefaults) {
150
150
  versionRange: versionRange || ((_a = projectConfigMap[projectId]) === null || _a === void 0 ? void 0 : _a.version) || "latest",
151
151
  componentIdOrNames: undefined,
152
152
  projectApiToken: projectApiToken || projectIdToToken.get(projectId),
153
+ indirect: false,
153
154
  };
154
155
  });
155
156
  const projectSyncParams = projectWithVersion.length
@@ -159,6 +160,7 @@ function sync(opts, metadataDefaults) {
159
160
  versionRange: p.version,
160
161
  componentIdOrNames: undefined,
161
162
  projectApiToken: p.projectApiToken,
163
+ indirect: !!p.indirect,
162
164
  }));
163
165
  // Short-circuit if nothing to sync
164
166
  if (projectSyncParams.length === 0) {
@@ -206,13 +208,15 @@ function sync(opts, metadataDefaults) {
206
208
  ...versionResolution.dependencies,
207
209
  ].map((p) => lodash_1.default.pick(p, "projectId", "projectApiToken"));
208
210
  context.api.attachProjectIdsAndTokens(projectIdsAndTokens);
211
+ const externalNpmPackages = new Set();
212
+ const externalCssImports = new Set();
209
213
  // Perform the actual sync
210
214
  yield file_utils_1.withBufferedFs(() => __awaiter(this, void 0, void 0, function* () {
211
215
  var _b;
212
216
  // Sync in sequence (no parallelism)
213
217
  // going in reverse to get leaves of the dependency tree first
214
218
  for (const projectMeta of projectsToSync) {
215
- yield syncProject(context, opts, projectIdsAndTokens, projectMeta.projectId, projectMeta.componentIds, projectMeta.version, projectMeta.dependencies, summary, pendingMerge, metadataDefaults);
219
+ yield syncProject(context, opts, projectIdsAndTokens, projectMeta.projectId, projectMeta.componentIds, projectMeta.version, projectMeta.dependencies, summary, pendingMerge, projectMeta.indirect, externalNpmPackages, externalCssImports, metadataDefaults);
216
220
  }
217
221
  // Materialize scheme into each component config.
218
222
  context.config.projects.forEach((p) => p.components.forEach((c) => {
@@ -221,11 +225,20 @@ function sync(opts, metadataDefaults) {
221
225
  }
222
226
  }));
223
227
  yield syncStyleConfig(context, yield context.api.genStyleConfig(context.config.style));
224
- // Update project version if specified and successfully synced.
228
+ // Update project version and indirect status if specified and
229
+ // successfully synced.
225
230
  if (projectWithVersion.length) {
226
231
  const versionMap = {};
227
232
  projectWithVersion.forEach((p) => (versionMap[p.projectId] = p.versionRange));
228
- context.config.projects.forEach((p) => (p.version = versionMap[p.projectId] || p.version));
233
+ const indirectMap = {};
234
+ projectsToSync.forEach((p) => (indirectMap[p.projectId] = p.indirect));
235
+ context.config.projects.forEach((p) => {
236
+ p.version = versionMap[p.projectId] || p.version;
237
+ // Only update `indirect` if it is set in current config.
238
+ if (p.projectId in indirectMap && p.indirect) {
239
+ p.indirect = indirectMap[p.projectId];
240
+ }
241
+ });
229
242
  }
230
243
  // Fix imports
231
244
  const fixImportContext = code_utils_1.mkFixImportContext(context.config);
@@ -244,9 +257,22 @@ function sync(opts, metadataDefaults) {
244
257
  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) });
245
258
  writeLoaderConfig(opts, config);
246
259
  }
260
+ const codegenVersion = yield context.api.latestCodegenVersion();
261
+ context.lock.projects.forEach(p => {
262
+ if (projectsToSync.some(syncedProject => syncedProject.projectId === p.projectId)) {
263
+ p.codegenVersion = codegenVersion;
264
+ }
265
+ });
247
266
  // Write the new ComponentConfigs to disk
248
267
  yield config_utils_1.updateConfig(context, context.config, baseDir);
249
268
  }));
269
+ yield checkExternalPkgs(context, baseDir, opts, Array.from(externalNpmPackages.keys()));
270
+ if (!opts.quiet && externalCssImports.size > 0) {
271
+ deps_1.logger.info(`This project uses external packages and styles. Make sure to import the following global CSS: ` +
272
+ Array.from(externalCssImports.keys())
273
+ .map((stmt) => `"${stmt}"`)
274
+ .join(", "));
275
+ }
250
276
  // Post-sync commands
251
277
  if (!opts.ignorePostSync) {
252
278
  for (const cmd of context.config.postSyncCommands || []) {
@@ -261,6 +287,20 @@ function sync(opts, metadataDefaults) {
261
287
  });
262
288
  }
263
289
  exports.sync = sync;
290
+ function checkExternalPkgs(context, baseDir, opts, pkgs) {
291
+ return __awaiter(this, void 0, void 0, function* () {
292
+ const missingPkgs = pkgs.filter((pkg) => {
293
+ const installedPkg = npm_utils_1.findInstalledVersion(context, baseDir, pkg);
294
+ return !installedPkg;
295
+ });
296
+ if (missingPkgs.length > 0) {
297
+ 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);
298
+ if (upgrade) {
299
+ npm_utils_1.installUpgrade(missingPkgs.join(" "), baseDir);
300
+ }
301
+ }
302
+ });
303
+ }
264
304
  function maybeRenamePathExt(context, path, ext) {
265
305
  if (!path) {
266
306
  return path;
@@ -281,7 +321,7 @@ function fixFileExtension(context) {
281
321
  });
282
322
  });
283
323
  }
284
- function syncProject(context, opts, projectIdsAndTokens, projectId, componentIds, projectVersion, dependencies, summary, pendingMerge, metadataDefaults) {
324
+ function syncProject(context, opts, projectIdsAndTokens, projectId, componentIds, projectVersion, dependencies, summary, pendingMerge, indirect, externalNpmPackages, externalCssImports, metadataDefaults) {
285
325
  var _a;
286
326
  return __awaiter(this, void 0, void 0, function* () {
287
327
  const newComponentScheme = opts.newComponentScheme || context.config.code.scheme;
@@ -301,6 +341,7 @@ function syncProject(context, opts, projectIdsAndTokens, projectId, componentIds
301
341
  checksums: existingChecksums,
302
342
  codeOpts: context.config.code,
303
343
  metadata: get_context_1.generateMetadata(Object.assign(Object.assign({}, metadataDefaults), { platform: context.config.platform }), opts.metadata),
344
+ indirect,
304
345
  });
305
346
  // Convert from TSX => JSX
306
347
  if (context.config.code.lang === "js") {
@@ -319,11 +360,13 @@ function syncProject(context, opts, projectIdsAndTokens, projectId, componentIds
319
360
  });
320
361
  }
321
362
  yield sync_global_variants_1.syncGlobalVariants(context, projectBundle.projectConfig, projectBundle.globalVariants, projectBundle.checksums, opts.baseDir);
322
- yield syncProjectConfig(context, projectBundle.projectConfig, projectApiToken, projectVersion, dependencies, projectBundle.components, opts.forceOverwrite, !!opts.appendJsxOnMissingBase, summary, pendingMerge, projectBundle.checksums, opts.baseDir);
363
+ yield syncProjectConfig(context, projectBundle.projectConfig, projectApiToken, projectVersion, dependencies, projectBundle.components, opts.forceOverwrite, !!opts.appendJsxOnMissingBase, summary, pendingMerge, projectBundle.checksums, opts.baseDir, indirect);
323
364
  syncCodeComponentsMeta(context, projectId, projectBundle.codeComponentMetas);
324
365
  yield sync_styles_1.upsertStyleTokens(context, projectBundle.usedTokens);
325
366
  yield sync_icons_1.syncProjectIconAssets(context, projectId, projectVersion, projectBundle.iconAssets, projectBundle.checksums, opts.baseDir);
326
367
  yield sync_images_1.syncProjectImageAssets(context, projectId, projectVersion, projectBundle.imageAssets, projectBundle.checksums);
368
+ (projectBundle.usedNpmPackages || []).forEach((pkg) => externalNpmPackages.add(pkg));
369
+ (projectBundle.externalCssImports || []).forEach((css) => externalCssImports.add(css));
327
370
  });
328
371
  }
329
372
  function syncStyleConfig(context, response) {
@@ -336,7 +379,7 @@ function syncStyleConfig(context, response) {
336
379
  });
337
380
  });
338
381
  }
339
- function syncProjectConfig(context, projectBundle, projectApiToken, version, dependencies, componentBundles, forceOverwrite, appendJsxOnMissingBase, summary, pendingMerge, checksums, baseDir) {
382
+ function syncProjectConfig(context, projectBundle, projectApiToken, version, dependencies, componentBundles, forceOverwrite, appendJsxOnMissingBase, summary, pendingMerge, checksums, baseDir, indirect) {
340
383
  return __awaiter(this, void 0, void 0, function* () {
341
384
  const defaultCssFilePath = file_utils_1.defaultResourcePath(context, projectBundle.projectName, projectBundle.cssFileName);
342
385
  const isNew = !context.config.projects.find((p) => p.projectId === projectBundle.projectId);
@@ -346,13 +389,13 @@ function syncProjectConfig(context, projectBundle, projectApiToken, version, dep
346
389
  projectName: projectBundle.projectName,
347
390
  version,
348
391
  cssFilePath: defaultCssFilePath,
392
+ indirect,
349
393
  }));
350
394
  // Update missing/outdated props
351
395
  projectConfig.projectName = projectBundle.projectName;
352
396
  if (!projectConfig.cssFilePath) {
353
397
  projectConfig.cssFilePath = defaultCssFilePath;
354
398
  }
355
- projectConfig.projectApiToken = projectApiToken;
356
399
  // plasmic.lock
357
400
  const projectLock = config_utils_1.getOrAddProjectLock(context, projectConfig.projectId);
358
401
  projectLock.version = version;
package/dist/api.d.ts CHANGED
@@ -58,6 +58,7 @@ export interface ProjectVersionMeta {
58
58
  dependencies: {
59
59
  [projectId: string]: string;
60
60
  };
61
+ indirect: boolean;
61
62
  }
62
63
  export interface VersionResolution {
63
64
  projects: ProjectVersionMeta[];
@@ -79,6 +80,8 @@ export interface ProjectBundle {
79
80
  iconAssets: IconBundle[];
80
81
  imageAssets: ImageBundle[];
81
82
  checksums: ChecksumBundle;
83
+ usedNpmPackages: string[];
84
+ externalCssImports: string[];
82
85
  }
83
86
  export declare type ProjectMeta = Omit<ProjectBundle, "projectConfig">;
84
87
  export interface StyleConfigResponse {
@@ -124,6 +127,7 @@ export interface ProjectIdAndToken {
124
127
  }
125
128
  export declare class PlasmicApi {
126
129
  private auth;
130
+ private codegenVersion?;
127
131
  constructor(auth: AuthConfig);
128
132
  genStyleConfig(styleOpts?: StyleConfig): Promise<StyleConfigResponse>;
129
133
  /**
@@ -143,6 +147,7 @@ export declare class PlasmicApi {
143
147
  }[], recursive?: boolean): Promise<VersionResolution>;
144
148
  getCurrentUser(): Promise<import("axios").AxiosResponse<any>>;
145
149
  requiredPackages(): Promise<RequiredPackages>;
150
+ latestCodegenVersion(): Promise<string>;
146
151
  /**
147
152
  * Code-gen endpoint.
148
153
  * This will fetch components at an exact specified version.
@@ -165,6 +170,7 @@ export declare class PlasmicApi {
165
170
  stylesOpts: StyleConfig;
166
171
  codeOpts: CodeConfig;
167
172
  checksums: ChecksumBundle;
173
+ indirect: boolean;
168
174
  metadata?: Metadata;
169
175
  }): Promise<ProjectBundle>;
170
176
  uploadBundle(projectId: string, bundleName: string, bundleJs: string, css: string[], metaJson: string, genModulePath: string | undefined, genCssPaths: string[], pkgVersion: string | undefined, extraPropMetaJson: string | undefined, themeProviderWrapper: string | undefined, themeModule: string | undefined): Promise<StyleTokensMap>;
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.
@@ -0,0 +1,2 @@
1
+ import { PlasmicConfig } from "../utils/config-utils";
2
+ export declare function ensureIndirect(config: PlasmicConfig): PlasmicConfig;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureIndirect = void 0;
4
+ function ensureIndirect(config) {
5
+ for (const p of config.projects) {
6
+ if (p.indirect === undefined) {
7
+ p.indirect = false;
8
+ }
9
+ }
10
+ return config;
11
+ }
12
+ exports.ensureIndirect = ensureIndirect;
@@ -40,6 +40,7 @@ const npm_utils_1 = require("../utils/npm-utils");
40
40
  const user_utils_1 = require("../utils/user-utils");
41
41
  const _0_1_110_fileLocks_1 = require("./0.1.110-fileLocks");
42
42
  const _0_1_146_addReactRuntime_1 = require("./0.1.146-addReactRuntime");
43
+ const _0_1_166_indirect_1 = require("./0.1.166-indirect");
43
44
  const _0_1_27_migrateInit_1 = require("./0.1.27-migrateInit");
44
45
  const _0_1_28_tsToTsx_1 = require("./0.1.28-tsToTsx");
45
46
  const _0_1_31_ensureProjectIcons_1 = require("./0.1.31-ensureProjectIcons");
@@ -56,6 +57,7 @@ exports.MIGRATIONS = {
56
57
  "0.1.64": _0_1_64_imageFiles_1.ensureImageFiles,
57
58
  "0.1.95": _0_1_95_componentType_1.ensureComponentType,
58
59
  "0.1.146": _0_1_146_addReactRuntime_1.ensureReactRuntime,
60
+ "0.1.166": _0_1_166_indirect_1.ensureIndirect,
59
61
  };
60
62
  exports.LOCK_MIGRATIONS = {
61
63
  "0.1.110": _0_1_110_fileLocks_1.ensureFileLocks,
@@ -296,6 +296,10 @@
296
296
  },
297
297
  "type": "array"
298
298
  },
299
+ "indirect": {
300
+ "description": "True if the project was installed indirectly (as a dependency); if set,\ncodegen will not generate pages.",
301
+ "type": "boolean"
302
+ },
299
303
  "jsBundleThemes": {
300
304
  "items": {
301
305
  "$ref": "#/definitions/JsBundleThemeConfig"
@@ -324,6 +328,7 @@
324
328
  "cssFilePath",
325
329
  "icons",
326
330
  "images",
331
+ "indirect",
327
332
  "projectId",
328
333
  "projectName",
329
334
  "version"
@@ -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;
@@ -138,12 +138,15 @@ exports.project1Config = {
138
138
  icons: [],
139
139
  images: [],
140
140
  jsBundleThemes: [],
141
+ indirect: false,
141
142
  };
142
- function expectProject1PlasmicJson() {
143
+ function expectProject1PlasmicJson(optional) {
143
144
  const plasmicJson = exports.tmpRepo.readPlasmicJson();
144
145
  expect(plasmicJson.projects.length).toEqual(1);
145
146
  const projectConfig = plasmicJson.projects[0];
146
- expect(projectConfig.projectApiToken).toBe("abc");
147
+ if (!(optional === null || optional === void 0 ? void 0 : optional.projectApiToken)) {
148
+ expect(projectConfig.projectApiToken).toBe("abc");
149
+ }
147
150
  expect(projectConfig.components.length).toEqual(2);
148
151
  const componentNames = projectConfig.components.map((c) => c.name);
149
152
  expect(componentNames).toContain("Button");
@@ -126,6 +126,11 @@ export interface ProjectConfig {
126
126
  icons: IconConfig[];
127
127
  /** Metadata for each synced image in this project */
128
128
  images: ImageConfig[];
129
+ /**
130
+ * True if the project was installed indirectly (as a dependency); if set,
131
+ * codegen will not generate pages.
132
+ */
133
+ indirect: boolean;
129
134
  }
130
135
  export declare function createProjectConfig(base: {
131
136
  projectId: string;
@@ -133,6 +138,7 @@ export declare function createProjectConfig(base: {
133
138
  projectName: string;
134
139
  version: string;
135
140
  cssFilePath: string;
141
+ indirect: boolean;
136
142
  }): ProjectConfig;
137
143
  export interface TokensConfig {
138
144
  scheme: "theo";
@@ -219,6 +225,7 @@ export interface ProjectLock {
219
225
  };
220
226
  lang: "ts" | "js";
221
227
  fileLocks: FileLock[];
228
+ codegenVersion?: string;
222
229
  }
223
230
  export interface PlasmicLock {
224
231
  projects: ProjectLock[];
@@ -39,6 +39,7 @@ function createProjectConfig(base) {
39
39
  components: [],
40
40
  icons: [],
41
41
  images: [],
42
+ indirect: base.indirect,
42
43
  };
43
44
  }
44
45
  exports.createProjectConfig = createProjectConfig;
@@ -148,6 +149,7 @@ function getOrAddProjectConfig(context, projectId, base // if one doesn't exist,
148
149
  icons: [],
149
150
  images: [],
150
151
  jsBundleThemes: [],
152
+ indirect: false,
151
153
  };
152
154
  context.config.projects.push(project);
153
155
  }
@@ -47,6 +47,7 @@ const user_utils_1 = require("./user-utils");
47
47
  * @param versionResolution
48
48
  */
49
49
  function walkDependencyTree(root, available) {
50
+ root.indirect = false;
50
51
  const queue = [root];
51
52
  const result = [];
52
53
  const getMeta = (projectId, version) => {
@@ -54,7 +55,7 @@ function walkDependencyTree(root, available) {
54
55
  if (!meta) {
55
56
  throw new Error(`Cannot find projectId=${projectId}, version=${version} in the sync resolution results.`);
56
57
  }
57
- return meta;
58
+ return Object.assign(Object.assign({}, meta), { indirect: meta.projectId !== root.projectId });
58
59
  };
59
60
  while (queue.length > 0) {
60
61
  const curr = lang_utils_1.ensure(queue.shift());
@@ -73,6 +74,16 @@ function checkProjectMeta(meta, root, context, opts) {
73
74
  const projectId = meta.projectId;
74
75
  const projectName = meta.projectName;
75
76
  const newVersion = meta.version;
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();
76
87
  // Checks newVersion against plasmic.lock
77
88
  const checkVersionLock = () => __awaiter(this, void 0, void 0, function* () {
78
89
  const projectLock = context.lock.projects.find((p) => p.projectId === projectId);
@@ -86,7 +97,9 @@ function checkProjectMeta(meta, root, context, opts) {
86
97
  meta !== root) {
87
98
  // If this is a dependency (not root), and we're dealing with latest dep version
88
99
  // just skip, it's confusing
89
- 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
+ }
90
103
  return false;
91
104
  }
92
105
  if (semver.isLatest(newVersion)) {
@@ -104,7 +117,9 @@ function checkProjectMeta(meta, root, context, opts) {
104
117
  return true;
105
118
  }
106
119
  else {
107
- 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
+ }
108
123
  return false;
109
124
  }
110
125
  }
@@ -145,6 +160,15 @@ function checkProjectMeta(meta, root, context, opts) {
145
160
  deps_1.logger.warn(`${projectName}@${newVersion} falls outside the range specified in ${config_utils_1.CONFIG_FILE_NAME} (${versionRange})\nTip: To avoid this warning in the future, update your ${config_utils_1.CONFIG_FILE_NAME}.`);
146
161
  return yield user_utils_1.confirmWithUser("Do you want to force it?", opts.force || opts.yes, "n");
147
162
  });
163
+ const checkIndirect = () => __awaiter(this, void 0, void 0, function* () {
164
+ const projectConfig = context.config.projects.find((p) => p.projectId === projectId);
165
+ const configIndirect = projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.indirect;
166
+ if (configIndirect && !indirect) {
167
+ deps_1.logger.warn(`'${projectName}' was synced indirectly before, but a direct sync was requested. If it has page components, they will be synced and saved to plasmic.json.`);
168
+ return yield user_utils_1.confirmWithUser("Do you want to confirm it?", opts.force || opts.yes, "n");
169
+ }
170
+ return true;
171
+ });
148
172
  const projectIds = opts.projects.length > 0
149
173
  ? opts.projects
150
174
  : context.config.projects.map((p) => p.projectId);
@@ -153,7 +177,24 @@ function checkProjectMeta(meta, root, context, opts) {
153
177
  // we should always sync it, even if nothing has changed
154
178
  return true;
155
179
  }
156
- return (yield checkVersionLock()) && (yield checkVersionRange());
180
+ const checkedVersion = (yield checkVersionLock()) &&
181
+ (yield checkVersionRange()) &&
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
+ }
157
198
  });
158
199
  }
159
200
  /**
@@ -175,13 +216,18 @@ function checkVersionResolution(versionResolution, context, opts) {
175
216
  ? [root]
176
217
  : walkDependencyTree(root, versionResolution.dependencies).reverse();
177
218
  for (const m of queue) {
178
- // If we haven't seen this yet
179
219
  if (!seen.find((p) => p.projectId === m.projectId)) {
180
220
  if (yield checkProjectMeta(m, root, context, opts)) {
181
221
  result.push(m);
182
222
  }
183
223
  seen.push(m);
184
224
  }
225
+ else if (root.projectId === m.projectId) {
226
+ // If m is the root project and it was already seen (maybe as a dep
227
+ // for another project), set indirect = false.
228
+ const project = lang_utils_1.ensure(seen.find((p) => p.projectId === m.projectId));
229
+ project.indirect = false;
230
+ }
185
231
  }
186
232
  }
187
233
  // Ignore repeats
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasmicapp/cli",
3
- "version": "0.1.165",
3
+ "version": "0.1.169",
4
4
  "description": "plasmic cli for syncing local code with Plasmic designs",
5
5
  "engines": {
6
6
  "node": ">=12"
@@ -61,6 +61,7 @@ function mockProjectToProjectVersionMeta(
61
61
  componentIdOrNames.includes(c.id)
62
62
  )
63
63
  .map((c) => c.id),
64
+ indirect: false,
64
65
  };
65
66
  }
66
67
 
@@ -339,6 +340,8 @@ class PlasmicApi {
339
340
  globalVariantChecksums: [],
340
341
  projectCssChecksum: "",
341
342
  } as ChecksumBundle,
343
+ usedNpmPackages: [],
344
+ externalCssImports: [],
342
345
  };
343
346
  return result;
344
347
  }
@@ -369,6 +372,10 @@ class PlasmicApi {
369
372
  throw new Error("Unimplemented");
370
373
  }
371
374
 
375
+ async latestCodegenVersion(): Promise<string> {
376
+ return "0.0.1";
377
+ };
378
+
372
379
  async requiredPackages(): Promise<RequiredPackages> {
373
380
  return {
374
381
  "@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 () => {
@@ -234,6 +234,7 @@ export async function sync(
234
234
  versionRange || projectConfigMap[projectId]?.version || "latest",
235
235
  componentIdOrNames: undefined, // Get all components!
236
236
  projectApiToken: projectApiToken || projectIdToToken.get(projectId),
237
+ indirect: false,
237
238
  };
238
239
  });
239
240
 
@@ -244,6 +245,7 @@ export async function sync(
244
245
  versionRange: p.version,
245
246
  componentIdOrNames: undefined, // Get all components!
246
247
  projectApiToken: p.projectApiToken,
248
+ indirect: !!p.indirect,
247
249
  }));
248
250
 
249
251
  // Short-circuit if nothing to sync
@@ -259,7 +261,7 @@ export async function sync(
259
261
  try {
260
262
  context = await getContext(opts);
261
263
  } catch (e) {
262
- if (e.message.includes("Unable to authenticate Plasmic")) {
264
+ if ((e as any).message.includes("Unable to authenticate Plasmic")) {
263
265
  const configFileName = process.env.PLASMIC_LOADER
264
266
  ? LOADER_CONFIG_FILE_NAME
265
267
  : CONFIG_FILE_NAME;
@@ -313,6 +315,8 @@ export async function sync(
313
315
  ].map((p) => L.pick(p, "projectId", "projectApiToken"));
314
316
 
315
317
  context.api.attachProjectIdsAndTokens(projectIdsAndTokens);
318
+ const externalNpmPackages = new Set<string>();
319
+ const externalCssImports = new Set<string>();
316
320
 
317
321
  // Perform the actual sync
318
322
  await withBufferedFs(async () => {
@@ -329,6 +333,9 @@ export async function sync(
329
333
  projectMeta.dependencies,
330
334
  summary,
331
335
  pendingMerge,
336
+ projectMeta.indirect,
337
+ externalNpmPackages,
338
+ externalCssImports,
332
339
  metadataDefaults
333
340
  );
334
341
  }
@@ -347,15 +354,22 @@ export async function sync(
347
354
  await context.api.genStyleConfig(context.config.style)
348
355
  );
349
356
 
350
- // Update project version if specified and successfully synced.
357
+ // Update project version and indirect status if specified and
358
+ // successfully synced.
351
359
  if (projectWithVersion.length) {
352
360
  const versionMap: Record<string, string> = {};
353
361
  projectWithVersion.forEach(
354
362
  (p) => (versionMap[p.projectId] = p.versionRange)
355
363
  );
356
- context.config.projects.forEach(
357
- (p) => (p.version = versionMap[p.projectId] || p.version)
358
- );
364
+ const indirectMap: Record<string, boolean> = {};
365
+ projectsToSync.forEach((p) => (indirectMap[p.projectId] = p.indirect));
366
+ context.config.projects.forEach((p) => {
367
+ p.version = versionMap[p.projectId] || p.version;
368
+ // Only update `indirect` if it is set in current config.
369
+ if (p.projectId in indirectMap && p.indirect) {
370
+ p.indirect = indirectMap[p.projectId];
371
+ }
372
+ });
359
373
  }
360
374
 
361
375
  // Fix imports
@@ -402,10 +416,32 @@ export async function sync(
402
416
  writeLoaderConfig(opts, config);
403
417
  }
404
418
 
419
+ const codegenVersion = await context.api.latestCodegenVersion();
420
+ context.lock.projects.forEach(p => {
421
+ if (projectsToSync.some(syncedProject => syncedProject.projectId === p.projectId)) {
422
+ p.codegenVersion = codegenVersion;
423
+ }
424
+ })
405
425
  // Write the new ComponentConfigs to disk
406
426
  await updateConfig(context, context.config, baseDir);
407
427
  });
408
428
 
429
+ await checkExternalPkgs(
430
+ context,
431
+ baseDir,
432
+ opts,
433
+ Array.from(externalNpmPackages.keys())
434
+ );
435
+
436
+ if (!opts.quiet && externalCssImports.size > 0) {
437
+ logger.info(
438
+ `This project uses external packages and styles. Make sure to import the following global CSS: ` +
439
+ Array.from(externalCssImports.keys())
440
+ .map((stmt) => `"${stmt}"`)
441
+ .join(", ")
442
+ );
443
+ }
444
+
409
445
  // Post-sync commands
410
446
  if (!opts.ignorePostSync) {
411
447
  for (const cmd of context.config.postSyncCommands || []) {
@@ -420,6 +456,30 @@ export async function sync(
420
456
  }
421
457
  }
422
458
 
459
+ async function checkExternalPkgs(
460
+ context: PlasmicContext,
461
+ baseDir: string,
462
+ opts: SyncArgs,
463
+ pkgs: string[]
464
+ ) {
465
+ const missingPkgs = pkgs.filter((pkg) => {
466
+ const installedPkg = findInstalledVersion(context, baseDir, pkg);
467
+ return !installedPkg;
468
+ });
469
+ if (missingPkgs.length > 0) {
470
+ const upgrade = await confirmWithUser(
471
+ `The following packages aren't installed but are required by some projects, would you like to install them? ${missingPkgs.join(
472
+ ", "
473
+ )}`,
474
+ opts.yes
475
+ );
476
+
477
+ if (upgrade) {
478
+ installUpgrade(missingPkgs.join(" "), baseDir);
479
+ }
480
+ }
481
+ }
482
+
423
483
  function maybeRenamePathExt(
424
484
  context: PlasmicContext,
425
485
  path: string,
@@ -469,6 +529,9 @@ async function syncProject(
469
529
  dependencies: { [projectId: string]: string },
470
530
  summary: Map<string, ComponentUpdateSummary>,
471
531
  pendingMerge: ComponentPendingMerge[],
532
+ indirect: boolean,
533
+ externalNpmPackages: Set<string>,
534
+ externalCssImports: Set<string>,
472
535
  metadataDefaults?: Metadata
473
536
  ): Promise<void> {
474
537
  const newComponentScheme =
@@ -512,6 +575,7 @@ async function syncProject(
512
575
  },
513
576
  opts.metadata
514
577
  ),
578
+ indirect,
515
579
  });
516
580
 
517
581
  // Convert from TSX => JSX
@@ -570,7 +634,8 @@ async function syncProject(
570
634
  summary,
571
635
  pendingMerge,
572
636
  projectBundle.checksums,
573
- opts.baseDir
637
+ opts.baseDir,
638
+ indirect
574
639
  );
575
640
  syncCodeComponentsMeta(context, projectId, projectBundle.codeComponentMetas);
576
641
  await upsertStyleTokens(context, projectBundle.usedTokens);
@@ -589,6 +654,12 @@ async function syncProject(
589
654
  projectBundle.imageAssets,
590
655
  projectBundle.checksums
591
656
  );
657
+ (projectBundle.usedNpmPackages || []).forEach((pkg) =>
658
+ externalNpmPackages.add(pkg)
659
+ );
660
+ (projectBundle.externalCssImports || []).forEach((css) =>
661
+ externalCssImports.add(css)
662
+ );
592
663
  }
593
664
 
594
665
  async function syncStyleConfig(
@@ -619,7 +690,8 @@ async function syncProjectConfig(
619
690
  summary: Map<string, ComponentUpdateSummary>,
620
691
  pendingMerge: ComponentPendingMerge[],
621
692
  checksums: ChecksumBundle,
622
- baseDir: string
693
+ baseDir: string,
694
+ indirect: boolean
623
695
  ) {
624
696
  const defaultCssFilePath = defaultResourcePath(
625
697
  context,
@@ -639,6 +711,7 @@ async function syncProjectConfig(
639
711
  projectName: projectBundle.projectName,
640
712
  version,
641
713
  cssFilePath: defaultCssFilePath,
714
+ indirect,
642
715
  })
643
716
  );
644
717
 
@@ -647,7 +720,6 @@ async function syncProjectConfig(
647
720
  if (!projectConfig.cssFilePath) {
648
721
  projectConfig.cssFilePath = defaultCssFilePath;
649
722
  }
650
- projectConfig.projectApiToken = projectApiToken;
651
723
 
652
724
  // plasmic.lock
653
725
  const projectLock = getOrAddProjectLock(context, projectConfig.projectId);
package/src/api.ts CHANGED
@@ -76,6 +76,7 @@ export interface ProjectVersionMeta {
76
76
  dependencies: {
77
77
  [projectId: string]: string;
78
78
  };
79
+ indirect: boolean;
79
80
  }
80
81
 
81
82
  export interface VersionResolution {
@@ -100,6 +101,8 @@ export interface ProjectBundle {
100
101
  iconAssets: IconBundle[];
101
102
  imageAssets: ImageBundle[];
102
103
  checksums: ChecksumBundle;
104
+ usedNpmPackages: string[];
105
+ externalCssImports: string[];
103
106
  }
104
107
 
105
108
  export type ProjectMeta = Omit<ProjectBundle, "projectConfig">;
@@ -158,6 +161,7 @@ export interface ProjectIdAndToken {
158
161
  }
159
162
 
160
163
  export class PlasmicApi {
164
+ private codegenVersion?: string;
161
165
  constructor(private auth: AuthConfig) {}
162
166
 
163
167
  async genStyleConfig(styleOpts?: StyleConfig): Promise<StyleConfigResponse> {
@@ -210,6 +214,16 @@ export class PlasmicApi {
210
214
  return { ...resp.data } as RequiredPackages;
211
215
  }
212
216
 
217
+ async latestCodegenVersion(): Promise<string> {
218
+ if (!this.codegenVersion) {
219
+ const resp = await this.post(
220
+ `${this.codegenHost}/api/v1/code/latest-codegen-version`
221
+ );
222
+ this.codegenVersion = resp.data as string;
223
+ }
224
+ return this.codegenVersion;
225
+ }
226
+
213
227
  /**
214
228
  * Code-gen endpoint.
215
229
  * This will fetch components at an exact specified version.
@@ -235,6 +249,7 @@ export class PlasmicApi {
235
249
  stylesOpts: StyleConfig;
236
250
  codeOpts: CodeConfig;
237
251
  checksums: ChecksumBundle;
252
+ indirect: boolean;
238
253
  metadata?: Metadata;
239
254
  }
240
255
  ): Promise<ProjectBundle> {
@@ -0,0 +1,10 @@
1
+ import { PlasmicConfig } from "../utils/config-utils";
2
+
3
+ export function ensureIndirect(config: PlasmicConfig) {
4
+ for (const p of config.projects) {
5
+ if (p.indirect === undefined) {
6
+ p.indirect = false;
7
+ }
8
+ }
9
+ return config;
10
+ }
@@ -35,6 +35,7 @@ import {
35
35
  import { confirmWithUser } from "../utils/user-utils";
36
36
  import { ensureFileLocks } from "./0.1.110-fileLocks";
37
37
  import { ensureReactRuntime } from "./0.1.146-addReactRuntime";
38
+ import { ensureIndirect } from "./0.1.166-indirect";
38
39
  import { migrateInit } from "./0.1.27-migrateInit";
39
40
  import { tsToTsx } from "./0.1.28-tsToTsx";
40
41
  import { ensureProjectIcons } from "./0.1.31-ensureProjectIcons";
@@ -64,6 +65,7 @@ export const MIGRATIONS: Record<string, MigrateConfigFunc> = {
64
65
  "0.1.64": ensureImageFiles,
65
66
  "0.1.95": ensureComponentType,
66
67
  "0.1.146": ensureReactRuntime,
68
+ "0.1.166": ensureIndirect,
67
69
  };
68
70
 
69
71
  export const LOCK_MIGRATIONS: Record<string, MigrateLockFunc> = {
@@ -147,13 +147,18 @@ export const project1Config: ProjectConfig = {
147
147
  icons: [],
148
148
  images: [],
149
149
  jsBundleThemes: [],
150
+ indirect: false,
150
151
  };
151
152
 
152
- export function expectProject1PlasmicJson() {
153
+ export function expectProject1PlasmicJson(
154
+ optional?: { [k in keyof ProjectConfig]?: boolean }
155
+ ) {
153
156
  const plasmicJson = tmpRepo.readPlasmicJson();
154
157
  expect(plasmicJson.projects.length).toEqual(1);
155
158
  const projectConfig = plasmicJson.projects[0];
156
- expect(projectConfig.projectApiToken).toBe("abc");
159
+ if (!optional?.projectApiToken) {
160
+ expect(projectConfig.projectApiToken).toBe("abc");
161
+ }
157
162
  expect(projectConfig.components.length).toEqual(2);
158
163
  const componentNames = projectConfig.components.map((c) => c.name);
159
164
  expect(componentNames).toContain("Button");
@@ -171,6 +171,12 @@ export interface ProjectConfig {
171
171
  icons: IconConfig[];
172
172
  /** Metadata for each synced image in this project */
173
173
  images: ImageConfig[];
174
+
175
+ /**
176
+ * True if the project was installed indirectly (as a dependency); if set,
177
+ * codegen will not generate pages.
178
+ */
179
+ indirect: boolean;
174
180
  }
175
181
 
176
182
  export function createProjectConfig(base: {
@@ -179,6 +185,7 @@ export function createProjectConfig(base: {
179
185
  projectName: string;
180
186
  version: string;
181
187
  cssFilePath: string;
188
+ indirect: boolean;
182
189
  }): ProjectConfig {
183
190
  return {
184
191
  projectId: base.projectId,
@@ -189,6 +196,7 @@ export function createProjectConfig(base: {
189
196
  components: [],
190
197
  icons: [],
191
198
  images: [],
199
+ indirect: base.indirect,
192
200
  };
193
201
  }
194
202
 
@@ -310,6 +318,8 @@ export interface ProjectLock {
310
318
  lang: "ts" | "js";
311
319
  // One for each file whose checksum is computed
312
320
  fileLocks: FileLock[];
321
+ // The version of Codegen when this project was written
322
+ codegenVersion?: string;
313
323
  }
314
324
 
315
325
  export interface PlasmicLock {
@@ -519,6 +529,7 @@ export function getOrAddProjectConfig(
519
529
  icons: [],
520
530
  images: [],
521
531
  jsBundleThemes: [],
532
+ indirect: false,
522
533
  };
523
534
  context.config.projects.push(project);
524
535
  }
@@ -1,4 +1,4 @@
1
- import { exec, execSync, spawnSync } from "child_process";
1
+ import { execSync, spawnSync } from "child_process";
2
2
  import glob from "fast-glob";
3
3
  import findupSync from "findup-sync";
4
4
  import latest from "latest-version";
@@ -1,4 +1,5 @@
1
1
  import L from "lodash";
2
+ import { InvalidatedProjectKind } from "typescript";
2
3
  import { SyncArgs } from "../actions/sync";
3
4
  import { ProjectVersionMeta, VersionResolution } from "../api";
4
5
  import { logger } from "../deps";
@@ -19,6 +20,7 @@ function walkDependencyTree(
19
20
  root: ProjectVersionMeta,
20
21
  available: ProjectVersionMeta[]
21
22
  ): ProjectVersionMeta[] {
23
+ root.indirect = false;
22
24
  const queue: ProjectVersionMeta[] = [root];
23
25
  const result: ProjectVersionMeta[] = [];
24
26
 
@@ -31,7 +33,10 @@ function walkDependencyTree(
31
33
  `Cannot find projectId=${projectId}, version=${version} in the sync resolution results.`
32
34
  );
33
35
  }
34
- return meta;
36
+ return {
37
+ ...meta,
38
+ indirect: meta.projectId !== root.projectId,
39
+ };
35
40
  };
36
41
 
37
42
  while (queue.length > 0) {
@@ -61,6 +66,22 @@ async function checkProjectMeta(
61
66
  const projectId = meta.projectId;
62
67
  const projectName = meta.projectName;
63
68
  const newVersion = meta.version;
69
+ const indirect = meta.indirect;
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();
64
85
 
65
86
  // Checks newVersion against plasmic.lock
66
87
  const checkVersionLock = async (): Promise<boolean> => {
@@ -81,9 +102,11 @@ async function checkProjectMeta(
81
102
  ) {
82
103
  // If this is a dependency (not root), and we're dealing with latest dep version
83
104
  // just skip, it's confusing
84
- logger.warn(
85
- `'${root.projectName}' depends on ${projectName}@${newVersion}. To update this project, explicitly specify this project for sync. Skipping...`
86
- );
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
+ }
87
110
  return false;
88
111
  }
89
112
 
@@ -105,9 +128,11 @@ async function checkProjectMeta(
105
128
  );
106
129
  return true;
107
130
  } else {
108
- logger.info(
109
- `Project '${projectName}'@${newVersion} is already up to date; skipping. (To force an update, run again with "--force")`
110
- );
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
+ }
111
136
  return false;
112
137
  }
113
138
  }
@@ -177,6 +202,24 @@ async function checkProjectMeta(
177
202
  );
178
203
  };
179
204
 
205
+ const checkIndirect = async (): Promise<boolean> => {
206
+ const projectConfig = context.config.projects.find(
207
+ (p) => p.projectId === projectId
208
+ );
209
+ const configIndirect = projectConfig?.indirect;
210
+ if (configIndirect && !indirect) {
211
+ logger.warn(
212
+ `'${projectName}' was synced indirectly before, but a direct sync was requested. If it has page components, they will be synced and saved to plasmic.json.`
213
+ );
214
+ return await confirmWithUser(
215
+ "Do you want to confirm it?",
216
+ opts.force || opts.yes,
217
+ "n"
218
+ );
219
+ }
220
+ return true;
221
+ };
222
+
180
223
  const projectIds =
181
224
  opts.projects.length > 0
182
225
  ? opts.projects
@@ -187,8 +230,29 @@ async function checkProjectMeta(
187
230
  // we should always sync it, even if nothing has changed
188
231
  return true;
189
232
  }
233
+ const checkedVersion =
234
+ (await checkVersionLock()) &&
235
+ (await checkVersionRange()) &&
236
+ (await checkIndirect());
190
237
 
191
- return (await checkVersionLock()) && (await checkVersionRange());
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
+ }
192
256
  }
193
257
 
194
258
  /**
@@ -216,12 +280,16 @@ export async function checkVersionResolution(
216
280
  ? [root]
217
281
  : walkDependencyTree(root, versionResolution.dependencies).reverse();
218
282
  for (const m of queue) {
219
- // If we haven't seen this yet
220
283
  if (!seen.find((p) => p.projectId === m.projectId)) {
221
284
  if (await checkProjectMeta(m, root, context, opts)) {
222
285
  result.push(m);
223
286
  }
224
287
  seen.push(m);
288
+ } else if (root.projectId === m.projectId) {
289
+ // If m is the root project and it was already seen (maybe as a dep
290
+ // for another project), set indirect = false.
291
+ const project = ensure(seen.find((p) => p.projectId === m.projectId));
292
+ project.indirect = false;
225
293
  }
226
294
  }
227
295
  }