@smallpen/local-package 0.1.0-alpha.5 → 0.1.0-alpha.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smallpen/local-package",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "dependencies": {
14
14
  "@jsquash/webp": "1.5.0",
15
- "@smallpen/core": "0.1.0-alpha.5",
15
+ "@smallpen/core": "0.1.0-alpha.8",
16
16
  "opentype.js": "2.0.0"
17
17
  },
18
18
  "repository": {
package/src/index.mjs CHANGED
@@ -12,13 +12,22 @@ export {
12
12
  defaultLibraryCacheRoot,
13
13
  openRemoteLibrary,
14
14
  } from "./remote-library.mjs";
15
- export { createEvidence, renderProjection } from "./render.mjs";
15
+ export {
16
+ createEvidence,
17
+ measureProjectionText,
18
+ renderProjection,
19
+ } from "./render.mjs";
16
20
  export {
17
21
  decodeFigmaClipboard,
18
22
  importDraft,
19
23
  writeDraftPackage,
20
24
  } from "./draft-import.mjs";
21
- export { createBlankPackage, initializeWorkspace } from "./initialize.mjs";
25
+ export {
26
+ createBlankPackage,
27
+ initializeBlankWorkspace,
28
+ initializeWorkspace,
29
+ } from "./initialize.mjs";
30
+ export { migrateThemesByCopy } from "./migrate-themes.mjs";
22
31
  export {
23
32
  applyOperationBatch,
24
33
  deleteFontFamily,
@@ -26,6 +35,7 @@ export {
26
35
  importFontVariant,
27
36
  importMedia,
28
37
  openPackage,
38
+ readBatchHistory,
29
39
  removeMedia,
30
40
  replayRecordedBatch,
31
41
  updateFontFamily,
@@ -1,14 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import {
3
- mkdir,
4
- mkdtemp,
5
- rename,
6
- rm,
7
- writeFile,
8
- } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
9
3
  import { basename, dirname, join, resolve } from "node:path";
10
4
 
11
- import { canonicalJSON, SmallPenError } from "@smallpen/core";
5
+ import {
6
+ canonicalJSON,
7
+ initializationTokenThemes,
8
+ SmallPenError,
9
+ } from "@smallpen/core";
12
10
 
13
11
  import { pathExists } from "./fs-utils.mjs";
14
12
  import { openPackage } from "./local-package.mjs";
@@ -43,7 +41,9 @@ function tokenDescriptor(name) {
43
41
  if (prefix === "color") return { path, type: "color", value: "#6750a4" };
44
42
  if (prefix === "opacity") return { path, type: "opacity", value: 1 };
45
43
  if (prefix === "radius") return { path, type: "border-radius", value: 8 };
46
- if (prefix === "font") return { path, type: "font-family", value: "Inter" };
44
+ if (prefix === "font") {
45
+ return { path, type: "font-family", value: "Source Sans Pro" };
46
+ }
47
47
  return {
48
48
  path,
49
49
  type: prefix === "spacing" ? "spacing" : "number",
@@ -51,29 +51,59 @@ function tokenDescriptor(name) {
51
51
  };
52
52
  }
53
53
 
54
- function setToken(root, descriptor, usedIds) {
55
- const segments = descriptor.path.split(".");
56
- let parent = root;
57
- for (const segment of segments.slice(0, -1)) {
58
- parent[segment] ??= {};
59
- parent = parent[segment];
54
+ // The starter token library: initial Tokens in the base set, and the token
55
+ // themes the theme-kind axes describe (see initializationTokenThemes).
56
+ function tokenLibrary(brief, libraryId) {
57
+ const usedIds = new Set();
58
+ const usedNames = new Set();
59
+ const baseTokens = [];
60
+ for (const name of brief.initialTokens) {
61
+ const descriptor = tokenDescriptor(name);
62
+ if (usedNames.has(descriptor.path)) continue;
63
+ usedNames.add(descriptor.path);
64
+ let id = `tok_${slug(descriptor.path)}`;
65
+ let suffix = 2;
66
+ while (usedIds.has(id)) {
67
+ id = `tok_${slug(descriptor.path)}_${suffix}`;
68
+ suffix += 1;
69
+ }
70
+ usedIds.add(id);
71
+ baseTokens.push({
72
+ description: "",
73
+ id,
74
+ name: descriptor.path,
75
+ type: descriptor.type,
76
+ value: descriptor.value,
77
+ });
60
78
  }
61
- let id = `tok_${slug(descriptor.path)}`;
62
- let suffix = 2;
63
- while (usedIds.has(id)) {
64
- id = `tok_${slug(descriptor.path)}_${suffix}`;
65
- suffix += 1;
66
- }
67
- usedIds.add(id);
68
- parent[segments.at(-1)] = {
69
- $extensions: { smallpen: { id, visibility: "public" } },
70
- $type: descriptor.type,
71
- $value: descriptor.value,
79
+ const { sets, themes } = initializationTokenThemes(brief.contextAxes);
80
+ return {
81
+ activeSetIds: ["tset_base"],
82
+ activeThemeIds: themes.filter(({ active }) => active).map(({ id }) => id),
83
+ defaultThemeIds: themes.filter(({ active }) => active).map(({ id }) => id),
84
+ defaultSetIds: ["tset_base"],
85
+ id: libraryId,
86
+ sets: sets.map(({ id, name }) => ({
87
+ description: "",
88
+ id,
89
+ name,
90
+ tokens: id === "tset_base" ? baseTokens : [],
91
+ })),
92
+ themes: themes.map(({ group, id, name, setIds }) => ({
93
+ description: "",
94
+ externalId: "",
95
+ group,
96
+ id,
97
+ isSource: false,
98
+ name,
99
+ setIds,
100
+ })),
72
101
  };
73
102
  }
74
103
 
75
- function foundationFiles(proposal) {
76
- const brief = proposal.brief;
104
+ // Context axes: the platform viewport plus every non-theme axis. Theme-kind
105
+ // axes are token themes instead.
106
+ function contextFile(brief) {
77
107
  const platformValues = brief.platforms.map((value) => ({
78
108
  id: slug(value),
79
109
  name: value,
@@ -87,18 +117,30 @@ function foundationFiles(proposal) {
87
117
  values: platformValues,
88
118
  },
89
119
  ...brief.contextAxes
90
- .filter(({ id }) => id !== "axis_platform")
120
+ .filter(({ id, kind }) => id !== "axis_platform" && kind !== "theme")
91
121
  .map(contextAxis),
92
122
  ];
93
- const defaults = Object.fromEntries(
94
- axes.map((axis) => [axis.id, axis.defaultValue]),
95
- );
96
- const tokens = {};
97
- const usedTokenIds = new Set();
98
- for (const token of brief.initialTokens) {
99
- setToken(tokens, tokenDescriptor(token), usedTokenIds);
100
- }
101
- const componentSets = brief.initialComponents.map((name) => {
123
+ return {
124
+ axes,
125
+ profiles: [
126
+ {
127
+ default: true,
128
+ id: `ctx_${slug(brief.platforms[0])}`,
129
+ name: brief.platforms[0],
130
+ values: Object.fromEntries(
131
+ axes.map((axis) => [axis.id, axis.defaultValue]),
132
+ ),
133
+ },
134
+ ],
135
+ };
136
+ }
137
+
138
+ function contextDefaults(brief) {
139
+ return contextFile(brief).profiles[0].values;
140
+ }
141
+
142
+ function starterComponentSets(brief) {
143
+ return brief.initialComponents.map((name) => {
102
144
  const id = slug(name);
103
145
  const nodeId = `node_${id}_root`;
104
146
  return {
@@ -128,6 +170,10 @@ function foundationFiles(proposal) {
128
170
  visibility: "public",
129
171
  };
130
172
  });
173
+ }
174
+
175
+ function foundationFiles(proposal) {
176
+ const brief = proposal.brief;
131
177
  return new Map([
132
178
  [
133
179
  "manifest.json",
@@ -147,64 +193,27 @@ function foundationFiles(proposal) {
147
193
  role: "foundation",
148
194
  },
149
195
  ],
150
- ["components/foundation.json", { componentSets }],
151
196
  [
152
- "contexts/foundation.json",
153
- {
154
- axes,
155
- profiles: [
156
- {
157
- default: true,
158
- id: `ctx_${slug(brief.platforms[0])}`,
159
- name: brief.platforms[0],
160
- values: defaults,
161
- },
162
- ],
163
- },
197
+ "components/foundation.json",
198
+ { componentSets: starterComponentSets(brief) },
164
199
  ],
165
- ["tokens/foundation.json", tokens],
200
+ ["contexts/foundation.json", contextFile(brief)],
201
+ ["tokens/foundation.json", tokenLibrary(brief, "tlib_foundation")],
166
202
  ]);
167
203
  }
168
204
 
169
- function productFiles(proposal) {
205
+ // Screens, Scenarios and requirements of the first design; `owner` is the
206
+ // Package that holds them.
207
+ function designFiles(proposal, owner) {
170
208
  const { brief, firstDesign } = proposal;
171
209
  const rootId = `node_${slug(brief.firstScreen)}_root`;
172
- const width = brief.platforms[0].toLowerCase().includes("mobile") ? 390 : 1024;
173
- const height = brief.platforms[0].toLowerCase().includes("mobile") ? 844 : 768;
174
- const defaults = {
175
- axis_platform: slug(brief.platforms[0]),
176
- ...Object.fromEntries(
177
- brief.contextAxes
178
- .filter(({ id }) => id !== "axis_platform")
179
- .map((axis) => [axis.id, slug(axis.defaultValue)]),
180
- ),
181
- };
210
+ const width = brief.platforms[0].toLowerCase().includes("mobile")
211
+ ? 390
212
+ : 1024;
213
+ const height = brief.platforms[0].toLowerCase().includes("mobile")
214
+ ? 844
215
+ : 768;
182
216
  return new Map([
183
- [
184
- "manifest.json",
185
- {
186
- defaultScreenId: firstDesign.screenId,
187
- dependencies: [
188
- {
189
- packageId: proposal.packages.foundation.packageId,
190
- path: proposal.packages.foundation.directoryName,
191
- },
192
- ],
193
- entries: {
194
- assets: [],
195
- components: [],
196
- contexts: [],
197
- requirements: ["requirements/product.json"],
198
- scenarios: ["scenarios/product.json"],
199
- screens: ["screens/first-design.json"],
200
- tokens: ["tokens/product.json"],
201
- },
202
- formatVersion: 1,
203
- name: proposal.packages.product.name,
204
- packageId: proposal.packages.product.packageId,
205
- role: "product",
206
- },
207
- ],
208
217
  [
209
218
  "requirements/product.json",
210
219
  {
@@ -232,7 +241,7 @@ function productFiles(proposal) {
232
241
  scenarios: [
233
242
  {
234
243
  actions: [],
235
- context: defaults,
244
+ context: contextDefaults(brief),
236
245
  expectedVisibleNodeIds: [rootId],
237
246
  fixture: {},
238
247
  id: firstDesign.scenarioId,
@@ -242,7 +251,7 @@ function productFiles(proposal) {
242
251
  presentationId: firstDesign.presentationId,
243
252
  screen: {
244
253
  assetId: firstDesign.screenId,
245
- packageId: proposal.packages.product.packageId,
254
+ packageId: owner.packageId,
246
255
  },
247
256
  },
248
257
  viewport: { height, scale: 1, width },
@@ -282,36 +291,117 @@ function productFiles(proposal) {
282
291
  ],
283
292
  },
284
293
  ],
294
+ ]);
295
+ }
296
+
297
+ function productFiles(proposal) {
298
+ const { firstDesign } = proposal;
299
+ const foundationThemes = tokenLibrary(proposal.brief, "tlib_foundation");
300
+ return new Map([
301
+ [
302
+ "manifest.json",
303
+ {
304
+ defaultScreenId: firstDesign.screenId,
305
+ dependencies: [
306
+ {
307
+ // The Product's selection of the Foundation's token themes.
308
+ activeThemeIds: foundationThemes.activeThemeIds,
309
+ packageId: proposal.packages.foundation.packageId,
310
+ path: proposal.packages.foundation.directoryName,
311
+ },
312
+ ],
313
+ entries: {
314
+ assets: [],
315
+ components: [],
316
+ contexts: [],
317
+ requirements: ["requirements/product.json"],
318
+ scenarios: ["scenarios/product.json"],
319
+ screens: ["screens/first-design.json"],
320
+ tokens: ["tokens/product.json"],
321
+ },
322
+ formatVersion: 1,
323
+ name: proposal.packages.product.name,
324
+ packageId: proposal.packages.product.packageId,
325
+ role: "product",
326
+ },
327
+ ],
328
+ ...designFiles(proposal, proposal.packages.product),
285
329
  [
286
330
  "tokens/product.json",
287
331
  {
288
- activeSetIds: [],
289
- activeThemeIds: ["theme_default"],
332
+ // Product sets sit on top of the Foundation's active sets by name.
333
+ activeSetIds: ["tset_product"],
334
+ activeThemeIds: [],
290
335
  id: "tlib_product",
291
336
  sets: [
292
337
  {
293
338
  description: "",
294
- id: "tset_theme_default",
295
- name: "Theme/Default",
339
+ id: "tset_product",
340
+ name: "product",
296
341
  tokens: [],
297
342
  },
298
343
  ],
299
- themes: [
300
- {
301
- description: "",
302
- externalId: "",
303
- group: "Theme",
304
- id: "theme_default",
305
- isSource: false,
306
- name: "Default",
307
- setIds: ["tset_theme_default"],
308
- },
309
- ],
344
+ themes: [],
310
345
  },
311
346
  ],
312
347
  ]);
313
348
  }
314
349
 
350
+ // The default layout: one self-contained Package with token sets and
351
+ // themes, Components and the first design.
352
+ function singlePackageFiles(proposal) {
353
+ const { brief, firstDesign } = proposal;
354
+ const owner = proposal.packages.package;
355
+ return new Map([
356
+ [
357
+ "manifest.json",
358
+ {
359
+ defaultScreenId: firstDesign.screenId,
360
+ entries: {
361
+ assets: [],
362
+ components: ["components/components.json"],
363
+ contexts: ["contexts/contexts.json"],
364
+ requirements: ["requirements/product.json"],
365
+ scenarios: ["scenarios/product.json"],
366
+ screens: ["screens/first-design.json"],
367
+ tokens: ["tokens/tokens.json"],
368
+ },
369
+ formatVersion: 1,
370
+ name: owner.name,
371
+ packageId: owner.packageId,
372
+ role: "foundation",
373
+ },
374
+ ],
375
+ [
376
+ "components/components.json",
377
+ { componentSets: starterComponentSets(brief) },
378
+ ],
379
+ ["contexts/contexts.json", contextFile(brief)],
380
+ ...designFiles(proposal, owner),
381
+ [
382
+ "tokens/tokens.json",
383
+ tokenLibrary(brief, `tlib_${owner.packageId.replace(/^pkg_/, "")}`),
384
+ ],
385
+ ]);
386
+ }
387
+
388
+ // A package without pages: the CLI draws the first one with page draw.
389
+ function withoutPages(files) {
390
+ const manifest = files.get("manifest.json");
391
+ for (const entry of manifest.entries.screens) files.delete(entry);
392
+ manifest.entries.screens = [];
393
+ delete manifest.defaultScreenId;
394
+ return files;
395
+ }
396
+
397
+ // The package file is named after --name when given ("Team App" ->
398
+ // team-app.smallpen), else after its folder.
399
+ function packageFileStem(name, folder) {
400
+ const slug = String(name ?? "").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase()
401
+ .replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
402
+ return slug || folder;
403
+ }
404
+
315
405
  async function writeFiles(packagePath, files) {
316
406
  for (const [entry, value] of files) {
317
407
  const output = join(packagePath, entry);
@@ -320,10 +410,10 @@ async function writeFiles(packagePath, files) {
320
410
  }
321
411
  }
322
412
 
323
- function blankPackageFiles(name) {
413
+ function blankPackageFiles(name, { neutral = false } = {}) {
324
414
  const suffix = randomUUID().replaceAll("-", "");
325
415
  const screenId = `scr_${suffix}`;
326
- const presentationId = `pres_${suffix}_desktop`;
416
+ const presentationId = `pres_${suffix}_${neutral ? "base" : "desktop"}`;
327
417
  const rootId = `node_${suffix}_root`;
328
418
  return new Map([
329
419
  [
@@ -356,7 +446,7 @@ function blankPackageFiles(name) {
356
446
  {
357
447
  id: presentationId,
358
448
  interactions: [],
359
- name: "Desktop",
449
+ name: neutral ? "Base" : "Desktop",
360
450
  nodes: {
361
451
  [rootId]: {
362
452
  children: [],
@@ -370,7 +460,7 @@ function blankPackageFiles(name) {
370
460
  y: 0,
371
461
  },
372
462
  },
373
- platform: "desktop",
463
+ ...(neutral ? {} : { platform: "desktop" }),
374
464
  rootId,
375
465
  viewport: { height: 768, width: 1024 },
376
466
  },
@@ -407,6 +497,113 @@ function blankPackageFiles(name) {
407
497
  ]);
408
498
  }
409
499
 
500
+ // CLI creation has no business brief. Keep the same blank model as the App,
501
+ // with one Base canvas; workspace publication remains an atomic rename.
502
+ export async function initializeBlankWorkspace(locator, options = {}) {
503
+ const workspacePath = resolve(locator);
504
+ const layout = options.layout ?? "single";
505
+ if (!["single", "foundation-product"].includes(layout)) {
506
+ throw new SmallPenError("invalid_init_layout", "Unknown workspace layout", {
507
+ validValues: ["foundation-product", "single"],
508
+ value: layout,
509
+ });
510
+ }
511
+ if (workspacePath.toLowerCase().endsWith(".smallpen")) {
512
+ if (layout !== "single") {
513
+ throw new SmallPenError(
514
+ "invalid_init_layout",
515
+ "A paired workspace requires a directory, not a .smallpen path",
516
+ );
517
+ }
518
+ const created = await createBlankPackage(workspacePath, {
519
+ ...options,
520
+ neutral: true,
521
+ });
522
+ return { ...created, layout, status: "initialized" };
523
+ }
524
+ if (workspacePath === dirname(workspacePath)) {
525
+ throw new SmallPenError(
526
+ "invalid_workspace_path",
527
+ "Workspace cannot be a filesystem root",
528
+ );
529
+ }
530
+ if (await pathExists(workspacePath)) {
531
+ throw new SmallPenError(
532
+ "workspace_already_exists",
533
+ `Workspace target already exists: ${workspacePath}`,
534
+ { workspacePath },
535
+ );
536
+ }
537
+ const folder = basename(workspacePath);
538
+ const name = String(options.name ?? folder).trim() || "Untitled";
539
+ if (name.length > 255) {
540
+ throw new SmallPenError(
541
+ "invalid_package_name",
542
+ "Package name must contain at most 255 characters",
543
+ );
544
+ }
545
+ await mkdir(dirname(workspacePath), { recursive: true });
546
+ const transaction = await mkdtemp(
547
+ join(dirname(workspacePath), ".smallpen-init-"),
548
+ );
549
+ const candidate = join(transaction, folder);
550
+ try {
551
+ const files = options.page === false ? withoutPages(blankPackageFiles(name, { neutral: true })) : blankPackageFiles(name, { neutral: true });
552
+ const stem = options.name !== undefined ? packageFileStem(options.name, folder) : folder;
553
+ if (layout === "single") {
554
+ const filename = `${stem}.smallpen`;
555
+ await writeFiles(join(candidate, filename), files);
556
+ await openPackage(join(candidate, filename));
557
+ await rename(candidate, workspacePath);
558
+ return {
559
+ layout,
560
+ packagePath: join(workspacePath, filename),
561
+ status: "initialized",
562
+ workspacePath,
563
+ };
564
+ }
565
+ const foundationName = `${stem}-foundation.smallpen`;
566
+ const productName = `${stem}.smallpen`;
567
+ const foundationFiles = blankPackageFiles(`${name} Foundation`, {
568
+ neutral: true,
569
+ });
570
+ const foundationManifest = foundationFiles.get("manifest.json");
571
+ for (const entry of foundationManifest.entries.screens)
572
+ foundationFiles.delete(entry);
573
+ foundationManifest.entries.screens = [];
574
+ delete foundationManifest.defaultScreenId;
575
+ const manifest = files.get("manifest.json");
576
+ manifest.role = "product";
577
+ manifest.dependencies = [
578
+ {
579
+ packageId: foundationManifest.packageId,
580
+ path: foundationName,
581
+ activeThemeIds: ["theme_default"],
582
+ },
583
+ ];
584
+ files.set("tokens/tokens.json", {
585
+ id: "tlib_product",
586
+ sets: [],
587
+ themes: [],
588
+ activeSetIds: [],
589
+ activeThemeIds: [],
590
+ });
591
+ await writeFiles(join(candidate, foundationName), foundationFiles);
592
+ await writeFiles(join(candidate, productName), files);
593
+ await openWorkspace(join(candidate, productName));
594
+ await rename(candidate, workspacePath);
595
+ return {
596
+ foundationPath: join(workspacePath, foundationName),
597
+ layout,
598
+ productPath: join(workspacePath, productName),
599
+ status: "initialized",
600
+ workspacePath,
601
+ };
602
+ } finally {
603
+ await rm(transaction, { recursive: true, force: true });
604
+ }
605
+ }
606
+
410
607
  export async function createBlankPackage(packageLocator, options = {}) {
411
608
  const packagePath = resolve(packageLocator);
412
609
  if (!packagePath.toLowerCase().endsWith(".smallpen")) {
@@ -436,7 +633,8 @@ export async function createBlankPackage(packageLocator, options = {}) {
436
633
  const transactionPath = await mkdtemp(join(parent, ".smallpen-create-"));
437
634
  const candidatePath = join(transactionPath, basename(packagePath));
438
635
  try {
439
- await writeFiles(candidatePath, blankPackageFiles(name));
636
+ const files = blankPackageFiles(name, options);
637
+ await writeFiles(candidatePath, options.page === false ? withoutPages(files) : files);
440
638
  await openPackage(candidatePath);
441
639
  await rename(candidatePath, packagePath);
442
640
  await rm(transactionPath, { force: true, recursive: true });
@@ -447,7 +645,11 @@ export async function createBlankPackage(packageLocator, options = {}) {
447
645
  }
448
646
  }
449
647
 
450
- export async function initializeWorkspace(workspaceLocator, proposal, options = {}) {
648
+ export async function initializeWorkspace(
649
+ workspaceLocator,
650
+ proposal,
651
+ options = {},
652
+ ) {
451
653
  if (options.confirmed !== true) {
452
654
  throw new SmallPenError(
453
655
  "initialization_confirmation_required",
@@ -474,14 +676,24 @@ export async function initializeWorkspace(workspaceLocator, proposal, options =
474
676
  await mkdir(parent, { recursive: true });
475
677
  const transactionPath = await mkdtemp(join(parent, ".smallpen-init-"));
476
678
  const candidatePath = join(transactionPath, basename(workspacePath));
477
- const foundationPath = join(
478
- candidatePath,
479
- proposal.packages.foundation.directoryName,
480
- );
481
- const productPath = join(candidatePath, proposal.packages.product.directoryName);
679
+ // Proposals written before layouts existed always describe the pair.
680
+ const single = (proposal.layout ?? "foundation-product") === "single";
482
681
  try {
483
- await writeFiles(foundationPath, foundationFiles(proposal));
484
- await writeFiles(productPath, productFiles(proposal));
682
+ if (single) {
683
+ await writeFiles(
684
+ join(candidatePath, proposal.packages.package.directoryName),
685
+ singlePackageFiles(proposal),
686
+ );
687
+ } else {
688
+ await writeFiles(
689
+ join(candidatePath, proposal.packages.foundation.directoryName),
690
+ foundationFiles(proposal),
691
+ );
692
+ await writeFiles(
693
+ join(candidatePath, proposal.packages.product.directoryName),
694
+ productFiles(proposal),
695
+ );
696
+ }
485
697
  await mkdir(join(candidatePath, "initialization"), { recursive: true });
486
698
  await writeFile(
487
699
  join(candidatePath, "initialization", "brief.json"),
@@ -493,15 +705,41 @@ export async function initializeWorkspace(workspaceLocator, proposal, options =
493
705
  canonicalJSON(proposal),
494
706
  "utf8",
495
707
  );
496
- await openPackage(foundationPath);
497
- await openWorkspace(productPath);
708
+ if (single) {
709
+ await openWorkspace(
710
+ join(candidatePath, proposal.packages.package.directoryName),
711
+ );
712
+ } else {
713
+ await openPackage(
714
+ join(candidatePath, proposal.packages.foundation.directoryName),
715
+ );
716
+ await openWorkspace(
717
+ join(candidatePath, proposal.packages.product.directoryName),
718
+ );
719
+ }
498
720
  await rename(candidatePath, workspacePath);
499
721
  await rm(transactionPath, { force: true, recursive: true });
722
+ const paths = single
723
+ ? {
724
+ layout: "single",
725
+ packagePath: join(
726
+ workspacePath,
727
+ proposal.packages.package.directoryName,
728
+ ),
729
+ }
730
+ : {
731
+ foundationPath: join(
732
+ workspacePath,
733
+ proposal.packages.foundation.directoryName,
734
+ ),
735
+ layout: "foundation-product",
736
+ productPath: join(
737
+ workspacePath,
738
+ proposal.packages.product.directoryName,
739
+ ),
740
+ };
500
741
  return {
501
- foundationPath: join(
502
- workspacePath,
503
- proposal.packages.foundation.directoryName,
504
- ),
742
+ ...paths,
505
743
  initializationBriefPath: join(
506
744
  workspacePath,
507
745
  "initialization",
@@ -512,7 +750,6 @@ export async function initializeWorkspace(workspaceLocator, proposal, options =
512
750
  "initialization",
513
751
  "proposal.json",
514
752
  ),
515
- productPath: join(workspacePath, proposal.packages.product.directoryName),
516
753
  status: "initialized",
517
754
  workspacePath,
518
755
  };