@nodii/dev 0.6.16 → 0.6.20

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/README.md CHANGED
@@ -181,12 +181,15 @@ one `--scope` per scope:
181
181
  | --- | --- |
182
182
  | `component:NODE_ID` | a component set or standalone component, at its top-level node (a variant id is taken as its set) |
183
183
  | `page:NODE_ID` | a whole Figma page: new screens, specimens, docs pages |
184
+ | `section:NODE_ID` | a Figma section and every node inside it (its own size and name too) |
184
185
  | `foundations` | the file's variables, text styles and effect styles; one holder at a time |
185
186
  | `create:PAGE_ID:NAME` | a new component, made on that page |
186
187
 
187
188
  A node id from a figma.com link (`12-34`) is read as Figma's `12:34`. Two
188
189
  sets conflict only when their scopes overlap: the same node, a page that holds
189
- a claimed component (or the reverse), or foundations already held. Dev
190
+ a claimed component or section (or the reverse), a section that holds a
191
+ claimed component or section (or the reverse), or foundations already held.
192
+ Sibling sections don't conflict. Dev
190
193
  refuses an overlap with `SCOPE_CONFLICT`; the CLI prints Dev's message, then
191
194
  the set holding the overlapping scope. `figma lock` without `--scope` locks the
192
195
  whole file, as older CLIs do, and that conflicts with every other set.
@@ -399,6 +402,29 @@ version. A set never submitted needs `--file`: the body `figma submit --file`
399
402
  would send. A cold run can outlast the edge's gateway; the CLI then says
400
403
  `DEV_TIMEOUT` and to run it again (Dev keeps the mains it read for 5 minutes).
401
404
 
405
+ ### Large sets: submit as a Dev job
406
+
407
+ `figma submit`, `figma resubmit` and `design check --change-set` send
408
+ `Prefer: respond-async`. Dev answers 202 with a job, and the CLI polls it
409
+ (`getDesignSubmitJob`) with backoff, printing progress on stderr, until it
410
+ ends. The output and exit code are the synchronous call's: a succeeded job
411
+ prints Dev's 200 body and warnings, a failed one its error and findings. A
412
+ large set's gate outlasts the edge's 30 s timeout, so this is the default.
413
+
414
+ - `a submit is already running: JOB`: Dev already runs a submit (or a dry run)
415
+ for this set (409 `SUBMIT_IN_PROGRESS` / `CONFORMANCE_IN_PROGRESS`). The CLI
416
+ polls that job instead of sending again, then exits 1 with the 409's code
417
+ and that job's outcome (`--json`: `details.result` or `details.error`):
418
+ this command's body was not sent, so read the set and run it again if it
419
+ still needs to go.
420
+ - `SUBMIT_JOB_LOST`: Dev restarted while the job ran. Run the same command again.
421
+ - `SUBMIT_JOB_UNREACHABLE`: polling lost the edge; the job runs on in Dev.
422
+ Read the set with `figma changeset show` before sending it again.
423
+ - `--sync` sends the old single request. If it (or a submit's 202) meets the
424
+ edge's 502 or 504, the CLI reads the set and says whether the submit landed;
425
+ it never resends. `SUBMIT_UNCONFIRMED` means it hasn't yet: check with
426
+ `figma changeset show` before submitting again.
427
+
402
428
  When `figma submit`, `figma resubmit` or the dry run hits
403
429
  `DS_CONFORMANCE_FAILED`, the CLI prints each finding and Dev's warnings. A
404
430
  submit Dev accepts prints its warnings too. On a stale version it also prints
package/dist/cli.js CHANGED
@@ -43,12 +43,13 @@ import {
43
43
  figmaScopeSet,
44
44
  figmaScopeSetText,
45
45
  figmaSubmit,
46
+ submitMode,
46
47
  isChangeSetId,
47
48
  scopeText,
48
49
  scopesFrom,
49
50
  conformanceLines,
50
51
  } from "./figma.js";
51
- export { figmaLock } from "./figma.js";
52
+ export { figmaLock, submitMode } from "./figma.js";
52
53
  import { docsCommand, rulebookCommand, rulesCommand } from "./upload.js";
53
54
  import { changeSetCommand, guidelineCommand } from "./guidelines.js";
54
55
  import { designCheck, designCheckText, designRoles, designRolesText } from "./design.js";
@@ -96,19 +97,23 @@ Usage: nodii-dev <command> [options] [--json]
96
97
  design check FILE_KEY|LINK [--nodes A,B] [--roles R,S] [--library]
97
98
  # the DS conformance check Dev's submit runs, before you submit;
98
99
  # reads Figma with FIGMA_PAT; exit 1 = DS_CONFORMANCE_FAILED or an error (--json: data.passed)
99
- design check --change-set CS [--file SUBMIT.json] [--figma-version V]
100
+ design check --change-set CS [--file SUBMIT.json] [--figma-version V] [--sync]
100
101
  # Dev's dry run of the submit gate on a set, with the image guard;
101
102
  # a stale version is a warning that prints when to retry
102
103
  figma lock FILE_KEY --title TEXT [--scope KIND:ID ...]
103
- # scopes: component:NODE page:NODE foundations create:PAGE:NAME;
104
+ # scopes: component:NODE page:NODE section:NODE foundations create:PAGE:NAME;
104
105
  # no --scope locks the whole file (DEC-0032)
105
106
  figma scope add CHANGE_SET --scope KIND:ID [--scope ...] # widen a held set's scopes
106
107
  figma scope set CHANGE_SET --scope KIND:ID [--scope ...] # narrow to exactly these; the rest frees
107
108
  figma publish-plan FILE_KEY # what to publish, and what to leave unchecked in Figma's dialog
108
109
  figma published FILE_KEY|CHANGE_SET --figma-version V # a file key marks every awaiting set published
109
110
  figma check|log-write|submit|apply|abandon|release
111
+ figma submit CHANGE_SET --file SUBMIT.json [--sync]
112
+ # submit, resubmit and design check --change-set run as a job in Dev
113
+ # and poll it (a large set outlasts the edge's timeout); --sync waits
114
+ # on one request instead. A 502/504 is checked against the set, never resent
110
115
  figma withdraw CHANGE_SET # holder takes back a submit: in_review -> editing (or a re-submit -> applying)
111
- figma resubmit CHANGE_SET PAYLOAD.json
116
+ figma resubmit CHANGE_SET PAYLOAD.json [--sync]
112
117
  # holder re-submits every rejected item: applying -> in_review, round + 1
113
118
  figma changeset show CHANGE_SET # items, variants, round, decision and comment (with the previous round's)
114
119
  figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
@@ -428,7 +433,7 @@ export async function main(argv = process.argv.slice(2)) {
428
433
  if (command === "design" && action === "roles") return emit(await designRoles(rest), json, designRolesText);
429
434
  if (command === "design" && action === "check") {
430
435
  if ((!rest[0] && f["change-set"] === undefined) || rest.length > 1)
431
- throw new CliError("USAGE", "design check FILE_KEY|LINK [--nodes A,B] [--roles R,S] [--library] | design check --change-set CS [--file SUBMIT.json] [--figma-version V]", 2);
436
+ throw new CliError("USAGE", "design check FILE_KEY|LINK [--nodes A,B] [--roles R,S] [--library] | design check --change-set CS [--file SUBMIT.json] [--figma-version V] [--sync]", 2);
432
437
  const result = await designCheck(rest[0], f);
433
438
  emit(result.payload, json, result.human ?? designCheckText);
434
439
  if (result.exitCode) {
@@ -646,7 +651,7 @@ export async function main(argv = process.argv.slice(2)) {
646
651
  if (action === "submit") {
647
652
  const payload = f.file ? JSON.parse(await readFile(f.file, "utf8")) : { figmaVersion: requireFlag(f, "figma-version") };
648
653
  // A 200 carries top-level warnings (STALE_FIGMA_VERSION): print them.
649
- return emit(await figmaSubmit(rest[0], payload), json, (p) =>
654
+ return emit(await figmaSubmit(rest[0], payload, { sync: submitMode(f) }), json, (p) =>
650
655
  [JSON.stringify(p.data, null, 2), ...conformanceLines({ warnings: p.warnings })].join("\n"));
651
656
  }
652
657
  if (action === "file") {
@@ -686,7 +691,7 @@ export async function main(argv = process.argv.slice(2)) {
686
691
  if (action === "withdraw")
687
692
  return emit(await figmaWithdraw(rest[0]), json, (p) => `Withdrew ${rest[0]}; it is back in ${p.data?.state ?? "editing"}.`);
688
693
  if (action === "resubmit")
689
- return emit(await figmaResubmit(rest[0], rest[1]), json, (p) =>
694
+ return emit(await figmaResubmit(rest[0], rest[1], { sync: submitMode(f) }), json, (p) =>
690
695
  [`Re-submitted ${rest[0]}; it is ${p.data.state}.`, figmaChangeSetText(p.data), ...conformanceLines({ warnings: p.warnings })].join("\n"));
691
696
  if (action === "changeset") {
692
697
  if (rest[0] !== "show" || !rest[1] || rest.length > 2) throw new CliError("USAGE", "figma changeset show CHANGE_SET", 2);
package/dist/core.js CHANGED
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
- export const VERSION = "0.6.16";
7
+ export const VERSION = "0.6.20";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
@@ -113,7 +113,7 @@ const staleCode = (raw) => {
113
113
  }
114
114
  };
115
115
 
116
- async function devFetch(method, path, bodyText, contentType, accept) {
116
+ async function devFetch(method, path, bodyText, contentType, accept, extra = {}) {
117
117
  const config = await readConfig();
118
118
  const auth = await authProvider();
119
119
  const url = requestUrl(config, path);
@@ -124,7 +124,7 @@ async function devFetch(method, path, bodyText, contentType, accept) {
124
124
  let refreshed = false;
125
125
  let adopted = false;
126
126
  for (;;) {
127
- const headers = { accept };
127
+ const headers = { accept, ...extra };
128
128
  if (bodyText !== undefined) headers["content-type"] = contentType;
129
129
  Object.assign(headers, await auth.headers({ method, path, body: bodyText ?? "" }));
130
130
  let response;
@@ -143,8 +143,9 @@ async function devFetch(method, path, bodyText, contentType, accept) {
143
143
  }
144
144
  }
145
145
 
146
- export async function devRawRequest(method, path, bodyText, contentType = "application/json") {
147
- const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json");
146
+ /** A JSON call that keeps the status: `{ status, payload }`. Errors throw as devRawRequest's do. */
147
+ export async function devExchange(method, path, bodyText, { contentType = "application/json", headers = {} } = {}) {
148
+ const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json", headers);
148
149
  let payload;
149
150
  try {
150
151
  payload = raw ? JSON.parse(raw) : {};
@@ -158,7 +159,11 @@ export async function devRawRequest(method, path, bodyText, contentType = "appli
158
159
  1,
159
160
  payload.details,
160
161
  );
161
- return payload;
162
+ return { status: response.status, payload };
163
+ }
164
+
165
+ export async function devRawRequest(method, path, bodyText, contentType = "application/json") {
166
+ return (await devExchange(method, path, bodyText, { contentType })).payload;
162
167
  }
163
168
 
164
169
  /** A Dev route that answers with text (e.g. a document body as text/markdown). Errors are Dev's JSON. */
@@ -173,6 +178,10 @@ export async function devTextRequest(method, path) {
173
178
  export const devRequest = (method, path, body) =>
174
179
  devRawRequest(method, path, body === undefined ? undefined : JSON.stringify(body));
175
180
 
181
+ /** A JSON call sent with `Prefer: respond-async` (RFC 7240): `{ status, payload }`, 202 when Dev took it as a job. */
182
+ export const devAsyncRequest = async (method, path, body) =>
183
+ devExchange(method, path, body === undefined ? undefined : JSON.stringify(body), { headers: { prefer: "respond-async" } });
184
+
176
185
  /** Proves the stored OAuth login works: a token refresh, then the agent read once registered. */
177
186
  export async function checkLogin() {
178
187
  const config = await readConfig();
@@ -184,7 +193,7 @@ export async function checkLogin() {
184
193
  export const encode = (value) => encodeURIComponent(value);
185
194
  // Switches never take the next token as their value, so
186
195
  // `docs upload --wait rulebook/` keeps rulebook/ as a path.
187
- const SWITCHES = new Set(["json", "wait", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted", "library"]);
196
+ const SWITCHES = new Set(["json", "wait", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts", "drafted", "library", "async", "sync"]);
188
197
  export function parseArgs(argv) {
189
198
  const flags = {};
190
199
  const args = [];
package/dist/design.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // library main.
9
9
  import { readFile } from "node:fs/promises";
10
10
  import { CliError, devRequest } from "./core.js";
11
- import { conformanceLines, figmaConformance, figmaFileKey } from "./figma.js";
11
+ import { conformanceLines, figmaConformance, figmaFileKey, submitMode } from "./figma.js";
12
12
  import { checkConformance, neededMains, VERSION as CHECK_VERSION } from "./vendor/ds-conformance.mjs";
13
13
 
14
14
  const FIGMA_API = "https://api.figma.com/v1";
@@ -159,7 +159,7 @@ export async function lastPublish(figmaGet, fileKey) {
159
159
  /**
160
160
  * The main of each instance, keyed by the instance's componentId (this file's
161
161
  * id for it), resolved as Dev's gate resolves it (Dev issue 92, conformance
162
- * 1.0.3), so both give one verdict:
162
+ * 1.0.4), so both give one verdict:
163
163
  * - a local component (remote: false) is read from this file;
164
164
  * - a remote one is found in the DS file's published listing, else by Figma's
165
165
  * lookup of its key, and is read only from a library Dev has registered
@@ -273,8 +273,10 @@ const nodeId = (x) => (/^\d+-\d+$/.test(x) ? x.replace("-", ":") : x);
273
273
  * finding fails; stale overrides (a value equal to the main's) are listed but
274
274
  * never fail (DEC-0035).
275
275
  */
276
- export async function designCheck(target, flags = {}, { request = devRequest, env = process.env, figmaGet, read } = {}) {
277
- if (flags["change-set"] !== undefined) return designCheckChangeSet(target, flags, { request, ...(read ? { read } : {}) });
276
+ export async function designCheck(target, flags = {}, { request = devRequest, send, progress, env = process.env, figmaGet, read } = {}) {
277
+ submitMode(flags);
278
+ if (flags["change-set"] !== undefined)
279
+ return designCheckChangeSet(target, flags, { request, ...(send ? { send } : {}), ...(progress ? { progress } : {}), ...(read ? { read } : {}) });
278
280
  // A --nodes that names nothing must not widen to every page.
279
281
  if (flags.nodes !== undefined && ([flags.nodes].flat().includes(true) || !list(flags.nodes).length))
280
282
  throw new CliError("USAGE", "--nodes needs node ids, e.g. --nodes 12:34,12:56", 2);
@@ -345,6 +347,8 @@ export async function designCheck(target, flags = {}, { request = devRequest, en
345
347
  // Same value through a different variable: the file is behind a library
346
348
  // republish. Accept the update in Figma (MW6-17); never a failure.
347
349
  libraryUpdatePending: result.libraryUpdatePending ?? [],
350
+ // What the app-UI gate leaves out (email), with its node count (Dev issue 116).
351
+ outOfScope: result.outOfScope ?? [], outOfScopeNodes: result.outOfScopeNodes ?? 0,
348
352
  warnings,
349
353
  };
350
354
  return { payload: { data }, exitCode: data.passed ? 0 : 1 };
@@ -367,8 +371,9 @@ export function designCheckText({ data }) {
367
371
  * plus the submit-time guards against the set's items: IMAGE_NOT_TRUECOLOR
368
372
  * refuses; STALE_FIGMA_VERSION is a warning. Changes no state.
369
373
  */
370
- export async function designCheckChangeSet(target, flags, { request = devRequest, read = (f) => readFile(f, "utf8") } = {}) {
374
+ export async function designCheckChangeSet(target, flags, { request = devRequest, send, progress, read = (f) => readFile(f, "utf8") } = {}) {
371
375
  const id = flags["change-set"];
376
+ const sync = submitMode(flags);
372
377
  if (id === true || !id) throw new CliError("USAGE", "--change-set needs a change set id, e.g. --change-set CS-123", 2);
373
378
  if (target !== undefined) throw new CliError("USAGE", "design check --change-set CS takes no file: Dev checks the set's own items", 2);
374
379
  if (flags["figma-version"] === true) throw new CliError("USAGE", "--figma-version needs a version id", 2);
@@ -387,17 +392,18 @@ export async function designCheckChangeSet(target, flags, { request = devRequest
387
392
  if (flags["figma-version"]) body = { ...body, figmaVersion: String(flags["figma-version"]) };
388
393
  let p;
389
394
  try {
390
- p = await figmaConformance(id, body, { request });
395
+ p = await figmaConformance(id, body, { request, sync, ...(send ? { send } : {}), ...(progress ? { progress } : {}) });
391
396
  } catch (error) {
392
397
  // A cold run reads every root and main from Figma and can outlast the
393
398
  // edge's gateway (504, an HTML page). Dev keeps the mains it read for 5
394
399
  // minutes, so the same run again usually answers.
395
- if (error?.code !== "HTTP_504") throw error;
396
- throw new CliError("DEV_TIMEOUT", `Dev's dry run of ${id} ran past the edge's gateway timeout while reading Figma; run it again (Dev keeps the library mains it read for 5 minutes)`, 1);
400
+ if (error?.code !== "HTTP_504" && error?.code !== "HTTP_502") throw error;
401
+ throw new CliError("DEV_TIMEOUT", `Dev's dry run of ${id} ran past the edge's gateway timeout while reading Figma; run it again${sync ? " without --sync, so Dev runs it as a job" : ""} (Dev keeps the library mains it read for 5 minutes)`, 1);
397
402
  }
398
403
  const r = p.data ?? {};
399
404
  const data = { changeSetId: id, checkVersion: r.version, passed: Boolean(r.passed), checked: r.checked, findings: r.findings ?? [],
400
- staleOverrides: r.staleOverrides ?? [], libraryUpdatePending: r.libraryUpdatePending ?? [], warnings: r.warnings ?? [], staleVersion: r.staleVersion ?? null };
405
+ staleOverrides: r.staleOverrides ?? [], libraryUpdatePending: r.libraryUpdatePending ?? [],
406
+ outOfScope: r.outOfScope ?? [], outOfScopeNodes: r.outOfScopeNodes ?? 0, warnings: r.warnings ?? [], staleVersion: r.staleVersion ?? null };
401
407
  return { payload: { data }, exitCode: data.passed ? 0 : 1, human: designCheckChangeSetText };
402
408
  }
403
409