@smallpen/local-package 0.1.0-alpha.5 → 0.1.0-alpha.9
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 +2 -2
- package/src/index.mjs +12 -2
- package/src/initialize.mjs +362 -125
- package/src/local-package.mjs +69 -13
- package/src/local-workspace-session.mjs +55 -1
- package/src/migrate-themes.mjs +116 -0
- package/src/render.mjs +1029 -436
- package/src/workspace.mjs +93 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smallpen/local-package",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.9",
|
|
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.
|
|
15
|
+
"@smallpen/core": "0.1.0-alpha.9",
|
|
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 {
|
|
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 {
|
|
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,
|
package/src/initialize.mjs
CHANGED
|
@@ -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 {
|
|
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")
|
|
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,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}
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
};
|