@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.
- package/assets/fonts/SourceSansPro-OFL.txt +46 -0
- package/assets/fonts/WorkSans-OFL.txt +46 -0
- package/package.json +4 -4
- package/runtime/fonts/OFL.txt +46 -0
- package/runtime/fonts/sourcesanspro-black.ttf +0 -0
- package/runtime/fonts/sourcesanspro-blackitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-bolditalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralight.ttf +0 -0
- package/runtime/fonts/sourcesanspro-extralightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-italic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-light.ttf +0 -0
- package/runtime/fonts/sourcesanspro-lightitalic.ttf +0 -0
- package/runtime/fonts/sourcesanspro-regular.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibold.ttf +0 -0
- package/runtime/fonts/sourcesanspro-semibolditalic.ttf +0 -0
- package/runtime/manifest.json +7 -0
- package/runtime/render-wasm.js +15 -0
- package/runtime/render-wasm.wasm +0 -0
- package/runtime/renderer.mjs +169652 -0
- package/src/draft-import.mjs +1 -11
- package/src/font-convert.mjs +23 -1
- package/src/fs-utils.mjs +95 -0
- package/src/index.mjs +19 -3
- package/src/initialize.mjs +363 -136
- package/src/kiwi-runtime.mjs +38 -36
- package/src/library-network.mjs +189 -0
- package/src/local-package-backend.mjs +215 -24
- package/src/local-package.mjs +589 -177
- package/src/local-workspace-session.mjs +278 -96
- package/src/migrate-themes.mjs +116 -0
- package/src/remote-library.mjs +167 -28
- package/src/render-fonts.mjs +172 -0
- package/src/render-shared.mjs +346 -0
- package/src/render.mjs +71 -3557
- package/src/workspace.mjs +178 -17
- package/src/webp-decode.mjs +0 -73
package/src/initialize.mjs
CHANGED
|
@@ -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 {
|
|
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")
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
94
|
-
axes
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
"
|
|
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
|
-
["
|
|
200
|
+
["contexts/foundation.json", contextFile(brief)],
|
|
201
|
+
["tokens/foundation.json", tokenLibrary(brief, "tlib_foundation")],
|
|
166
202
|
]);
|
|
167
203
|
}
|
|
168
204
|
|
|
169
|
-
|
|
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")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
289
|
-
|
|
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: "
|
|
295
|
-
name: "
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
};
|