@smallpen/local-package 0.1.0-alpha.1 → 0.1.0-alpha.11

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.
Files changed (37) hide show
  1. package/assets/fonts/SourceSansPro-OFL.txt +46 -0
  2. package/assets/fonts/WorkSans-OFL.txt +46 -0
  3. package/package.json +4 -4
  4. package/runtime/fonts/OFL.txt +46 -0
  5. package/runtime/fonts/sourcesanspro-black.ttf +0 -0
  6. package/runtime/fonts/sourcesanspro-blackitalic.ttf +0 -0
  7. package/runtime/fonts/sourcesanspro-bold.ttf +0 -0
  8. package/runtime/fonts/sourcesanspro-bolditalic.ttf +0 -0
  9. package/runtime/fonts/sourcesanspro-extralight.ttf +0 -0
  10. package/runtime/fonts/sourcesanspro-extralightitalic.ttf +0 -0
  11. package/runtime/fonts/sourcesanspro-italic.ttf +0 -0
  12. package/runtime/fonts/sourcesanspro-light.ttf +0 -0
  13. package/runtime/fonts/sourcesanspro-lightitalic.ttf +0 -0
  14. package/runtime/fonts/sourcesanspro-regular.ttf +0 -0
  15. package/runtime/fonts/sourcesanspro-semibold.ttf +0 -0
  16. package/runtime/fonts/sourcesanspro-semibolditalic.ttf +0 -0
  17. package/runtime/manifest.json +7 -0
  18. package/runtime/render-wasm.js +15 -0
  19. package/runtime/render-wasm.wasm +0 -0
  20. package/runtime/renderer.mjs +169652 -0
  21. package/src/draft-import.mjs +1 -11
  22. package/src/font-convert.mjs +23 -1
  23. package/src/fs-utils.mjs +95 -0
  24. package/src/index.mjs +19 -3
  25. package/src/initialize.mjs +363 -136
  26. package/src/kiwi-runtime.mjs +38 -36
  27. package/src/library-network.mjs +189 -0
  28. package/src/local-package-backend.mjs +215 -24
  29. package/src/local-package.mjs +589 -177
  30. package/src/local-workspace-session.mjs +278 -96
  31. package/src/migrate-themes.mjs +116 -0
  32. package/src/remote-library.mjs +167 -28
  33. package/src/render-fonts.mjs +172 -0
  34. package/src/render-shared.mjs +346 -0
  35. package/src/render.mjs +71 -3557
  36. package/src/workspace.mjs +178 -17
  37. package/src/webp-decode.mjs +0 -73
@@ -1,16 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import {
3
- lstat,
4
- mkdir,
5
- mkdtemp,
6
- rename,
7
- rm,
8
- writeFile,
9
- } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
10
3
  import { basename, dirname, join, resolve } from "node:path";
11
4
 
12
- import { canonicalJSON, SmallPenError } from "@smallpen/core";
5
+ import {
6
+ canonicalJSON,
7
+ initializationTokenThemes,
8
+ SmallPenError,
9
+ } from "@smallpen/core";
13
10
 
11
+ import { pathExists } from "./fs-utils.mjs";
14
12
  import { openPackage } from "./local-package.mjs";
15
13
  import { openWorkspace } from "./workspace.mjs";
16
14
 
@@ -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,20 +410,10 @@ async function writeFiles(packagePath, files) {
320
410
  }
321
411
  }
322
412
 
323
- async function pathExists(path) {
324
- try {
325
- await lstat(path);
326
- return true;
327
- } catch (error) {
328
- if (error?.code === "ENOENT") return false;
329
- throw error;
330
- }
331
- }
332
-
333
- function blankPackageFiles(name) {
413
+ function blankPackageFiles(name, { neutral = false } = {}) {
334
414
  const suffix = randomUUID().replaceAll("-", "");
335
415
  const screenId = `scr_${suffix}`;
336
- const presentationId = `pres_${suffix}_desktop`;
416
+ const presentationId = `pres_${suffix}_${neutral ? "base" : "desktop"}`;
337
417
  const rootId = `node_${suffix}_root`;
338
418
  return new Map([
339
419
  [
@@ -366,7 +446,7 @@ function blankPackageFiles(name) {
366
446
  {
367
447
  id: presentationId,
368
448
  interactions: [],
369
- name: "Desktop",
449
+ name: neutral ? "Base" : "Desktop",
370
450
  nodes: {
371
451
  [rootId]: {
372
452
  children: [],
@@ -380,7 +460,7 @@ function blankPackageFiles(name) {
380
460
  y: 0,
381
461
  },
382
462
  },
383
- platform: "desktop",
463
+ ...(neutral ? {} : { platform: "desktop" }),
384
464
  rootId,
385
465
  viewport: { height: 768, width: 1024 },
386
466
  },
@@ -417,6 +497,113 @@ function blankPackageFiles(name) {
417
497
  ]);
418
498
  }
419
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
+
420
607
  export async function createBlankPackage(packageLocator, options = {}) {
421
608
  const packagePath = resolve(packageLocator);
422
609
  if (!packagePath.toLowerCase().endsWith(".smallpen")) {
@@ -446,7 +633,8 @@ export async function createBlankPackage(packageLocator, options = {}) {
446
633
  const transactionPath = await mkdtemp(join(parent, ".smallpen-create-"));
447
634
  const candidatePath = join(transactionPath, basename(packagePath));
448
635
  try {
449
- await writeFiles(candidatePath, blankPackageFiles(name));
636
+ const files = blankPackageFiles(name, options);
637
+ await writeFiles(candidatePath, options.page === false ? withoutPages(files) : files);
450
638
  await openPackage(candidatePath);
451
639
  await rename(candidatePath, packagePath);
452
640
  await rm(transactionPath, { force: true, recursive: true });
@@ -457,7 +645,11 @@ export async function createBlankPackage(packageLocator, options = {}) {
457
645
  }
458
646
  }
459
647
 
460
- export async function initializeWorkspace(workspaceLocator, proposal, options = {}) {
648
+ export async function initializeWorkspace(
649
+ workspaceLocator,
650
+ proposal,
651
+ options = {},
652
+ ) {
461
653
  if (options.confirmed !== true) {
462
654
  throw new SmallPenError(
463
655
  "initialization_confirmation_required",
@@ -484,14 +676,24 @@ export async function initializeWorkspace(workspaceLocator, proposal, options =
484
676
  await mkdir(parent, { recursive: true });
485
677
  const transactionPath = await mkdtemp(join(parent, ".smallpen-init-"));
486
678
  const candidatePath = join(transactionPath, basename(workspacePath));
487
- const foundationPath = join(
488
- candidatePath,
489
- proposal.packages.foundation.directoryName,
490
- );
491
- 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";
492
681
  try {
493
- await writeFiles(foundationPath, foundationFiles(proposal));
494
- 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
+ }
495
697
  await mkdir(join(candidatePath, "initialization"), { recursive: true });
496
698
  await writeFile(
497
699
  join(candidatePath, "initialization", "brief.json"),
@@ -503,15 +705,41 @@ export async function initializeWorkspace(workspaceLocator, proposal, options =
503
705
  canonicalJSON(proposal),
504
706
  "utf8",
505
707
  );
506
- await openPackage(foundationPath);
507
- 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
+ }
508
720
  await rename(candidatePath, workspacePath);
509
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
+ };
510
741
  return {
511
- foundationPath: join(
512
- workspacePath,
513
- proposal.packages.foundation.directoryName,
514
- ),
742
+ ...paths,
515
743
  initializationBriefPath: join(
516
744
  workspacePath,
517
745
  "initialization",
@@ -522,7 +750,6 @@ export async function initializeWorkspace(workspaceLocator, proposal, options =
522
750
  "initialization",
523
751
  "proposal.json",
524
752
  ),
525
- productPath: join(workspacePath, proposal.packages.product.directoryName),
526
753
  status: "initialized",
527
754
  workspacePath,
528
755
  };